Skip to content

Commit 939f046

Browse files
feat: out of sync handling for dev mode
1 parent 2af31a9 commit 939f046

File tree

7 files changed

+133
-34
lines changed

7 files changed

+133
-34
lines changed

src/Device.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ class Device {
9191
),
9292
wasConnected: createStateObject(pymakr.context.workspaceState, `pymakr.devices.${this.id}.wasConnected`, true),
9393
pymakrConf: createStateObject(pymakr.context.globalState, `pymakr.devices.${this.id}.pymakrConf`, pymakrConfType),
94+
/** Last time this project was updated by dev upload. If value doesn't match project, device is considered stale */
95+
devUploadedAt: createStateObject(pymakr.context.globalState, `project.${this.id}.devUploadedAt`),
9496
/** @type {import("./utils/storageObj").GetterSetter<import("micropython-ctl-cont").BoardInfo>} */
9597
info: createStateObject(pymakr.context.globalState, `pymakr.devices.${this.id}.info`),
9698
};

src/Project.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const { readFileSync } = require("fs");
22
const { dirname, basename } = require("path");
3-
const vscode = require("vscode");
43
const { createStateObject } = require("./utils/storageObj");
54
const { Watcher } = require("./Watcher/Watcher");
65

@@ -16,9 +15,19 @@ class Project {
1615
this.folder = dirname(configFile.fsPath);
1716
this.watcher = new Watcher(this);
1817

19-
this.deviceIds = createStateObject(pymakr.context.globalState, `project.${this.folder}`, []);
20-
this.name = this.config.name || basename(this.folder);
18+
this.deviceIds = createStateObject(pymakr.context.globalState, `project.${this.folder}.deviceIds`, []);
19+
this.updatedAt = createStateObject(pymakr.context.globalState, `project.${this.folder}.updatedAt`);
20+
2121
this.log = pymakr.log.createChild("project: " + this.name);
22+
this.refresh();
23+
}
24+
25+
refresh() {
26+
this.name = this.config.name || basename(this.folder);
27+
}
28+
29+
destroy() {
30+
this.watcher.destroy();
2231
}
2332

2433
get config() {

src/Watcher/DeviceManager.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,31 @@ class DeviceManager {
2424
this.isRunning = false;
2525
}
2626

27+
get outOfSync() {
28+
return this.device.state.devUploadedAt.get() !== this.watcher.project.updatedAt.get();
29+
}
30+
31+
get shouldUploadOnDev() {
32+
const uploadWhen = this.watcher.project.config.dev?.uploadOnDevStart || "outOfSync";
33+
return uploadWhen === "always" || (uploadWhen === "outOfSync" && this.outOfSync);
34+
}
35+
36+
async uploadProjectIfNeeded() {
37+
if (!this.device.adapter.__proxyMeta.target.isConnected()) return;
38+
39+
const answer = await this.device.pymakr.notifier.notifications.deviceIsOutOfSync(this);
40+
41+
if (this.shouldUploadOnDev || answer === "upload")
42+
await this.device.pymakr.commands.uploadProject({ device: this.device, project: this.watcher.project });
43+
}
44+
2745
/**
2846
* Send a change/create/delete file instruction to the device
2947
* @param {FileInstruction} fileInstruction
3048
*/
3149
push(fileInstruction) {
3250
this.fileInstructions.push(fileInstruction);
33-
this.handleNewInstructions();
51+
return this.handleNewInstructions();
3452
}
3553

3654
async handleNewInstructions() {
@@ -44,7 +62,7 @@ class DeviceManager {
4462
this.log.debug("device/script restart completed");
4563

4664
// If new instructions were added while we restarted the device/script, let's rerun.
47-
if (this.fileInstructions.length) this.handleNewInstructions();
65+
if (this.fileInstructions.length) await this.handleNewInstructions();
4866
}
4967

5068
/**

src/Watcher/Watcher.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Watcher {
1313
/** @type {vscode.Disposable[]} */
1414
this.disposables = [];
1515
this.active = false;
16+
this.registerFileWatchers();
1617
}
1718

1819
/**
@@ -22,24 +23,16 @@ class Watcher {
2223
if (!this.deviceManagers.map((dm) => dm.device).includes(device))
2324
this.deviceManagers.push(new DeviceManager(this, device));
2425

25-
if (this.deviceManagers.length) this.start();
26+
if (this.deviceManagers.length) this.active = true;
27+
this.deviceManagers.find((dm) => dm.device === device)?.uploadProjectIfNeeded();
2628
}
2729

2830
removeDevice(device) {
2931
this.deviceManagers = [...this.deviceManagers.filter((dm) => dm.device !== device)];
30-
if (!this.deviceManagers.length) this.stop();
32+
if (!this.deviceManagers.length) this.active = false;
3133
}
3234

33-
/**
34-
* called whenever addDevice is called
35-
*/
36-
start() {
37-
if (this.active) return;
38-
this.active = true;
39-
this.onStart();
40-
}
41-
42-
onStart() {
35+
registerFileWatchers() {
4336
// this.deviceManagers = this.devices.map((device) => new DeviceManager(this, device));
4437
this.watcher = vscode.workspace.createFileSystemWatcher(this.project.folder + "/**");
4538
this.disposables = [
@@ -50,20 +43,25 @@ class Watcher {
5043
];
5144
}
5245

53-
stop() {
46+
destroy() {
5447
this.disposables.forEach((d) => d.dispose());
55-
this.active = false;
5648
}
5749

5850
/**
5951
* @param {'create'|'change'|'delete'} action
6052
* @returns {(file: vscode.Uri)=>void}
6153
*/
6254
handleFileChange(action) {
63-
return async (file) => {
64-
this.deviceManagers
65-
.filter((dm) => dm.device.adapter.__proxyMeta.target.isConnected())
66-
.forEach((manager) => manager.push({ file: file.fsPath, action }));
55+
return (file) => {
56+
const timestamp = new Date();
57+
this.project.updatedAt.set(timestamp);
58+
if (this.active)
59+
this.deviceManagers
60+
.filter((dm) => dm.device.adapter.__proxyMeta.target.isConnected())
61+
.forEach(async (manager) => {
62+
await manager.push({ file: file.fsPath, action });
63+
manager.device.state.devUploadedAt.set(timestamp);
64+
});
6765
};
6866
}
6967
}

src/commands/index.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -544,17 +544,28 @@ class Commands {
544544
* @param {{ device: Device, project: Project }} treeItem
545545
*/
546546
uploadProject: async ({ device, project }) => {
547-
if (project.watcher.active) {
548-
const deviceManager = project.watcher.deviceManagers.find((d) => d.device === device);
549-
deviceManager.push({ action: "create", file: project.folder });
550-
this.pymakr.notifier.notifications.uploadInDevMode();
551-
} else {
547+
if (project.watcher.active) return this.commands.uploadProjectDev({ device, project });
548+
else {
549+
this.log.debug("uploadProject", device, project);
552550
await device.adapter.remove(device.config.rootPath, true);
553551
this.commands.upload({ fsPath: project.folder }, device, "/");
554552
this.pymakr.notifier.notifications.uploadProject();
555553
}
556554
},
557555

556+
/**
557+
* Uploads parent project to the device. Can only be accessed from devices in the projects view.
558+
* @param {{ device: Device, project: Project }} treeItem
559+
*/
560+
uploadProjectDev: async ({ device, project }) => {
561+
this.log.debug("uploadDevProject", device, project);
562+
const deviceManager = project.watcher.deviceManagers.find((d) => d.device === device);
563+
this.pymakr.notifier.notifications.uploadInDevMode();
564+
const timestamp = project.updatedAt.get();
565+
await deviceManager.push({ action: "create", file: project.folder });
566+
deviceManager.device.state.devUploadedAt.set(timestamp);
567+
},
568+
558569
/**
559570
* Prompts for a device and destination for uploading a file or folder
560571
* @param {vscode.Uri} uri

src/stores/projects.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,35 @@ const { Project } = require("../Project");
33
const { writable } = require("../utils/store");
44

55
/**
6-
* @param {PyMakr} pymakr
6+
* @param {import('../PyMakr').PyMakr} pymakr
7+
* @returns {(configFile: import('vscode').Uri)=>Project}
78
*/
8-
const getProjects = async (pymakr) => {
9-
const configFiles = await workspace.findFiles("**/pymakr.conf");
10-
return configFiles.map((configFile) => new Project(configFile, pymakr));
11-
};
9+
const convertToProject = (pymakr) => (configFile) => new Project(configFile, pymakr);
10+
11+
/** @param {Project[]} projects */
12+
const isNotIn = (projects) => (configFile) => !projects.find((p) => p.configFile.path === configFile.path);
13+
14+
/**
15+
* @param {Project} p1
16+
* @param {Project} p2
17+
*/
18+
const byName = (p1, p2) => (p1.name < p2.name ? 1 : p1.name > p2.name ? -1 : 0);
19+
20+
/**
21+
* @param {import('vscode').Uri[]} configFiles
22+
* @returns {(project:Project) => Boolean}
23+
*/
24+
const hasConfigFile = (configFiles) => (project) => configFiles.map((cf) => cf.path).includes(project.configFile.path);
25+
26+
/**
27+
* @param {import('vscode').Uri[]} configFiles
28+
* @returns {(project:Project) => Boolean}
29+
*/
30+
const hasNoConfigFile = (configFiles) => (project) =>
31+
!configFiles.map((cfg) => cfg.path).includes(project.configFile.path);
32+
33+
/** @param {Project} project */
34+
const destroy = (project) => project.destroy();
1235

1336
/**
1437
* @param {PyMakr} pymakr
@@ -19,7 +42,15 @@ const createProjectsStore = (pymakr) => {
1942
/** Rescan for projects in workspace. */
2043
const refresh = async () => {
2144
pymakr.log.debug("Refreshing projects store...");
22-
store.set(await getProjects(pymakr));
45+
const configFiles = await workspace.findFiles("**/pymakr.conf");
46+
store.get().filter(hasNoConfigFile(configFiles)).forEach(destroy);
47+
store.update((oldProjects) =>
48+
[
49+
...oldProjects.filter(hasConfigFile(configFiles)),
50+
...configFiles.filter(isNotIn(oldProjects)).map(convertToProject(pymakr)),
51+
].sort(byName)
52+
);
53+
store.get().forEach((p) => p.refresh());
2354
pymakr.log.debug("Refreshing projects store. Complete!");
2455
};
2556

src/utils/Notifier.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,36 @@ class Notifier {
200200
"": [null, this.DONT_SHOW_AGAIN],
201201
}
202202
),
203+
204+
/**
205+
*
206+
* @param {import('../Watcher/DeviceManager').DeviceManager} deviceManager
207+
* @returns
208+
*/
209+
deviceIsOutOfSync: (deviceManager) => {
210+
if (deviceManager.outOfSync && deviceManager.shouldUploadOnDev)
211+
this.createNotification(
212+
"info",
213+
`[DEV] Uploaded project to "${deviceManager.device.name}", since device appeared to be out of sync.`,
214+
{ "": [null, this.DONT_SHOW_AGAIN] }
215+
);
216+
else if (deviceManager.shouldUploadOnDev)
217+
this.createNotification(
218+
"info",
219+
`[DEV] Uploaded project to "${deviceManager.device.name}" since "uploadOnDevStart" is set to "always".`,
220+
{ "": [null, this.DONT_SHOW_AGAIN] }
221+
);
222+
else if (deviceManager.outOfSync)
223+
return this.createNotification(
224+
"warning",
225+
"[DEV] Device is out of sync. Consider changing 'dev.uploadOnDevStart' in 'pymakr.conf'.",
226+
{ "": [null, this.DONT_SHOW_AGAIN], upload: `Upload project to ${deviceManager.device.name}` }
227+
);
228+
else
229+
this.createNotification("info", `[DEV] Device "${deviceManager.device.name}" is up to date.`, {
230+
"": [null, this.DONT_SHOW_AGAIN],
231+
});
232+
},
203233
};
204234
}
205235

0 commit comments

Comments
 (0)