electron-ohos-hotreload 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ /**
3
+ * FileWatcher.js — 主机侧文件监听
4
+ *
5
+ * 使用 Node.js 内置 fs.watch (Windows 支持 recursive: true)
6
+ * 跨平台兼容: Windows 用 recursive, 其他平台用递归遍历
7
+ */
8
+ var fs = require("fs");
9
+ var path = require("path");
10
+ var utils = require("../utils");
11
+
12
+ function FileWatcher(config) {
13
+ this.config = config;
14
+ this.watchers = [];
15
+ this.callback = null;
16
+ this.debounceTimer = null;
17
+ this.debounceMs = 500;
18
+ }
19
+
20
+ /** 设置变更回调 */
21
+ FileWatcher.prototype.onChange = function (cb) {
22
+ this.callback = cb;
23
+ };
24
+
25
+ /** 启动监听 */
26
+ FileWatcher.prototype.start = function () {
27
+ var self = this;
28
+ var dirs = this.extractWatchDirs();
29
+
30
+ var isWin = process.platform === "win32";
31
+
32
+ dirs.forEach(function (dir) {
33
+ if (!fs.existsSync(dir)) {
34
+ // 目录不存在,监听父目录
35
+ return;
36
+ }
37
+
38
+ try {
39
+ if (isWin) {
40
+ // Windows: recursive watch
41
+ var w = fs.watch(dir, { recursive: true }, function (eventType, filename) {
42
+ self.handleEvent(dir, filename);
43
+ });
44
+ self.watchers.push(w);
45
+ } else {
46
+ // Linux/macOS: 递归遍历
47
+ self.watchRecursive(dir);
48
+ }
49
+ } catch (e) {
50
+ // fs.watch 失败,回退到轮询
51
+ }
52
+ });
53
+
54
+ // 兜底轮询
55
+ self.startPolling(dirs);
56
+ };
57
+
58
+ /** 从 watch 模式提取监听目录 */
59
+ FileWatcher.prototype.extractWatchDirs = function () {
60
+ var dirs = [];
61
+ var seen = {};
62
+ var patterns = this.config.watch || ["spec/**/*.ts"];
63
+
64
+ patterns.forEach(function (pattern) {
65
+ // 取模式中的目录部分
66
+ var dirPart = pattern.replace(/\/[^/]*$/, "");
67
+ if (dirPart.indexOf("/") >= 0) {
68
+ dirPart = dirPart.split("/")[0];
69
+ }
70
+ if (!dirPart || dirPart.indexOf("*") >= 0) dirPart = ".";
71
+ if (!seen[dirPart]) {
72
+ seen[dirPart] = true;
73
+ dirs.push(dirPart);
74
+ }
75
+ });
76
+
77
+ // 转绝对路径
78
+ var root = this.config.hapRoot;
79
+ return dirs.map(function (d) {
80
+ return path.isAbsolute(d) ? d : path.join(root, d);
81
+ }).filter(function (d) { return fs.existsSync(d); });
82
+ };
83
+
84
+ /** 递归 watch (Linux/macOS) */
85
+ FileWatcher.prototype.watchRecursive = function (dir) {
86
+ var self = this;
87
+ try {
88
+ var w = fs.watch(dir, { recursive: false }, function (eventType, filename) {
89
+ self.handleEvent(dir, filename);
90
+ });
91
+ self.watchers.push(w);
92
+ } catch (e) {}
93
+
94
+ try {
95
+ var entries = fs.readdirSync(dir, { withFileTypes: true });
96
+ entries.forEach(function (entry) {
97
+ if (entry.isDirectory()) {
98
+ if (entry.name === "node_modules" || entry.name === ".git") return;
99
+ self.watchRecursive(path.join(dir, entry.name));
100
+ }
101
+ });
102
+ } catch (e) {}
103
+ };
104
+
105
+ /** 处理文件事件 */
106
+ FileWatcher.prototype.handleEvent = function (baseDir, filename) {
107
+ if (!filename) return;
108
+ if (!this.isWatchable(filename)) return;
109
+
110
+ var self = this;
111
+ clearTimeout(this.debounceTimer);
112
+ this.debounceTimer = setTimeout(function () {
113
+ if (self.callback) {
114
+ var full = path.join(baseDir, filename);
115
+ self.callback(full);
116
+ }
117
+ }, this.debounceMs);
118
+ };
119
+
120
+ /** 判断文件是否需要监听 */
121
+ FileWatcher.prototype.isWatchable = function (filename) {
122
+ // 排除
123
+ if (filename.indexOf("node_modules") >= 0) return false;
124
+ if (filename.indexOf(".git") >= 0) return false;
125
+ if (filename.indexOf("dist") >= 0) return false;
126
+
127
+ // 只关心 .ts .js 文件
128
+ if (!filename.endsWith(".ts") && !filename.endsWith(".js")) return false;
129
+
130
+ // 排除 entry.js 本身 (产物)
131
+ if (filename === "entry.js") return false;
132
+
133
+ return true;
134
+ };
135
+
136
+ /** 兜底轮询 */
137
+ FileWatcher.prototype.startPolling = function (dirs) {
138
+ var self = this;
139
+ var snapshots = {};
140
+ var interval = (this.config.pollInterval || 2000);
141
+
142
+ function snapshot(dir) {
143
+ var result = {};
144
+ function walk(d) {
145
+ try {
146
+ var entries = fs.readdirSync(d, { withFileTypes: true });
147
+ entries.forEach(function (entry) {
148
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") return;
149
+ var full = path.join(d, entry.name);
150
+ if (entry.isDirectory()) {
151
+ walk(full);
152
+ } else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
153
+ try {
154
+ var stat = fs.statSync(full);
155
+ result[full] = stat.mtimeMs;
156
+ } catch (e) {}
157
+ }
158
+ });
159
+ } catch (e) {}
160
+ }
161
+ walk(dir);
162
+ return result;
163
+ }
164
+
165
+ // 初始快照
166
+ dirs.forEach(function (dir) { snapshots[dir] = snapshot(dir); });
167
+
168
+ setInterval(function () {
169
+ dirs.forEach(function (dir) {
170
+ var current = snapshot(dir);
171
+ var prev = snapshots[dir] || {};
172
+ var changed = false;
173
+
174
+ Object.keys(current).forEach(function (file) {
175
+ if (prev[file] !== current[file]) {
176
+ changed = true;
177
+ }
178
+ });
179
+
180
+ if (changed) {
181
+ snapshots[dir] = current;
182
+ clearTimeout(self.debounceTimer);
183
+ self.debounceTimer = setTimeout(function () {
184
+ if (self.callback) self.callback("(polling)");
185
+ }, self.debounceMs);
186
+ } else {
187
+ snapshots[dir] = current;
188
+ }
189
+ });
190
+ }, interval);
191
+ };
192
+
193
+ /** 停止监听 */
194
+ FileWatcher.prototype.stop = function () {
195
+ this.watchers.forEach(function (w) {
196
+ try { w.close(); } catch (e) {}
197
+ });
198
+ this.watchers = [];
199
+ };
200
+
201
+ module.exports = FileWatcher;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ /**
3
+ * HdcBridge.js — hdc 命令封装
4
+ *
5
+ * 自动查找 hdc 可执行文件:
6
+ * 1) DEVECO_SDK_HOME/toolchains/hdc[.exe]
7
+ * 2) 常见 Windows 路径
8
+ * 3) PATH 中的 hdc
9
+ */
10
+ var fs = require("fs");
11
+ var path = require("path");
12
+ var os = require("os");
13
+ var { execFile } = require("child_process");
14
+
15
+ /** 查找 hdc */
16
+ function findHdc() {
17
+ var isWin = os.platform() === "win32";
18
+ var isMac = os.platform() === "darwin";
19
+ var exe = isWin ? "hdc.exe" : "hdc";
20
+
21
+ // 1) DEVECO_SDK_HOME
22
+ var sdkHome = process.env.DEVECO_SDK_HOME;
23
+ if (sdkHome) {
24
+ // 尝试多个可能的子路径
25
+ var subPaths = [
26
+ path.join(sdkHome, "toolchains", exe),
27
+ path.join(sdkHome, "default", "openharmony", "toolchains", exe),
28
+ path.join(sdkHome, "default", "hms", "toolchains", exe),
29
+ ];
30
+ for (var i = 0; i < subPaths.length; i++) {
31
+ if (fs.existsSync(subPaths[i])) return subPaths[i];
32
+ }
33
+ }
34
+
35
+ // 2) 常见路径
36
+ var commonPaths;
37
+ if (isWin) {
38
+ commonPaths = [
39
+ "D:\\Program Files\\Huawei\\DevEco Studio\\sdk\\toolchains\\hdc.exe",
40
+ "D:\\Program Files\\Huawei\\DevEco Studio\\sdk\\default\\openharmony\\toolchains\\hdc.exe",
41
+ "C:\\Program Files\\Huawei\\DevEco Studio\\sdk\\toolchains\\hdc.exe",
42
+ "D:\\Tools\\Huawei\\sdk\\toolchains\\hdc.exe",
43
+ ];
44
+ } else if (isMac) {
45
+ commonPaths = [
46
+ "/Applications/DevEco-Studio.app/Contents/sdk/toolchains/hdc",
47
+ "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc",
48
+ "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/toolchains/hdc",
49
+ "/opt/huawei/sdk/toolchains/hdc",
50
+ ];
51
+ } else {
52
+ commonPaths = [
53
+ "/opt/huawei/sdk/toolchains/hdc",
54
+ ];
55
+ }
56
+ for (var j = 0; j < commonPaths.length; j++) {
57
+ if (fs.existsSync(commonPaths[j])) return commonPaths[j];
58
+ }
59
+
60
+ // 3) PATH
61
+ return "hdc";
62
+ }
63
+
64
+ /** 执行命令 (Promise) */
65
+ function exec(cmd, args, opts) {
66
+ opts = opts || {};
67
+ return new Promise(function (resolve, reject) {
68
+ execFile(cmd, args, {
69
+ timeout: opts.timeout || 30000,
70
+ maxBuffer: 20 * 1024 * 1024,
71
+ windowsHide: true,
72
+ }, function (err, stdout, stderr) {
73
+ if (err) {
74
+ var msg = cmd + " " + args.join(" ") + " failed: " + (err.message || err);
75
+ if (stderr) msg += "\nstderr: " + stderr.trim();
76
+ reject(new Error(msg));
77
+ } else {
78
+ resolve({ stdout: stdout || "", stderr: stderr || "" });
79
+ }
80
+ });
81
+ });
82
+ }
83
+
84
+ function HdcBridge(bundleName) {
85
+ this.bundleName = bundleName;
86
+ this.hdcPath = findHdc();
87
+ }
88
+
89
+ /** hdc shell <command> */
90
+ HdcBridge.prototype.shell = function (command, timeout) {
91
+ return exec(this.hdcPath, ["shell", command], { timeout: timeout || 30000 })
92
+ .then(function (r) { return (r.stdout || "").trim(); });
93
+ };
94
+
95
+ /** hdc file send <local> <remote> */
96
+ HdcBridge.prototype.fileSend = function (localPath, remotePath) {
97
+ return exec(this.hdcPath, ["file", "send", localPath, remotePath])
98
+ .then(function (r) { return r.stdout.trim(); });
99
+ };
100
+
101
+ /** hdc file recv <remote> <local> */
102
+ HdcBridge.prototype.fileRecv = function (remotePath, localPath) {
103
+ return exec(this.hdcPath, ["file", "recv", remotePath, localPath])
104
+ .then(function (r) { return r.stdout.trim(); });
105
+ };
106
+
107
+ /** hdc shell mkdir -p */
108
+ HdcBridge.prototype.mkdir = function (remotePath) {
109
+ return this.shell("mkdir -p " + remotePath);
110
+ };
111
+
112
+ /** hdc shell touch */
113
+ HdcBridge.prototype.touch = function (remotePath) {
114
+ return this.shell("touch " + remotePath);
115
+ };
116
+
117
+ /** hdc shell cat */
118
+ HdcBridge.prototype.cat = function (remotePath) {
119
+ return this.shell("cat " + remotePath);
120
+ };
121
+
122
+ /** hdc shell rm -f */
123
+ HdcBridge.prototype.rm = function (remotePath) {
124
+ return this.shell("rm -f " + remotePath);
125
+ };
126
+
127
+ /** force-stop 应用 */
128
+ HdcBridge.prototype.forceStop = function () {
129
+ return this.shell("aa force-stop " + this.bundleName);
130
+ };
131
+
132
+ /** 启动 Ability */
133
+ HdcBridge.prototype.startAbility = function (ability) {
134
+ ability = ability || "EntryAbility";
135
+ return this.shell("aa start -a " + ability + " -b " + this.bundleName);
136
+ };
137
+
138
+ /** 安装 HAP */
139
+ HdcBridge.prototype.install = function (hapPath) {
140
+ return exec(this.hdcPath, ["install", hapPath])
141
+ .then(function (r) { return r.stdout.trim(); });
142
+ };
143
+
144
+ /** 卸载 */
145
+ HdcBridge.prototype.uninstall = function () {
146
+ return exec(this.hdcPath, ["uninstall", this.bundleName])
147
+ .then(function (r) { return r.stdout.trim(); });
148
+ };
149
+
150
+ /** 检查设备是否连接 */
151
+ HdcBridge.prototype.isDeviceConnected = function () {
152
+ return exec(this.hdcPath, ["list", "targets"])
153
+ .then(function (r) {
154
+ var lines = (r.stdout || "").trim().split("\n");
155
+ return lines.some(function (l) {
156
+ l = l.trim();
157
+ return l.length > 0 && l !== "[Empty]" && l.indexOf("No") !== 0;
158
+ });
159
+ })
160
+ .catch(function () { return false; });
161
+ };
162
+
163
+ /** 推送文件到 override 目录 (使用物理路径) */
164
+ HdcBridge.prototype.pushOverride = function (localPath, remoteName, config) {
165
+ var remote = config.physicalOverrideDir + "/" + remoteName;
166
+ return this.fileSend(localPath, remote);
167
+ };
168
+
169
+ /** 读取结果文件 (使用物理路径) */
170
+ HdcBridge.prototype.readResult = function (fileName, config) {
171
+ var remote = config.physicalResultDir + "/" + fileName;
172
+ return this.cat(remote).catch(function () { return ""; });
173
+ };
174
+
175
+ module.exports = HdcBridge;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * StatusPoller.js — 设备状态轮询
4
+ *
5
+ * 定期通过 hdc cat 读取设备上的 hot-status.json,
6
+ * 状态变化时通过事件回调通知上层。
7
+ */
8
+ var HdcBridge = require("./HdcBridge");
9
+
10
+ function StatusPoller(hdc, config) {
11
+ this.hdc = hdc;
12
+ this.config = config;
13
+ this.lastStatus = null;
14
+ this.timer = null;
15
+ this.listeners = { status: [], error: [] };
16
+ }
17
+
18
+ /** 注册事件回调 */
19
+ StatusPoller.prototype.on = function (event, cb) {
20
+ if (!this.listeners[event]) this.listeners[event] = [];
21
+ this.listeners[event].push(cb);
22
+ };
23
+
24
+ StatusPoller.prototype.emit = function (event, data) {
25
+ var cbs = this.listeners[event] || [];
26
+ for (var i = 0; i < cbs.length; i++) {
27
+ try { cbs[i](data); } catch (e) {}
28
+ }
29
+ };
30
+
31
+ /** 启动轮询 */
32
+ StatusPoller.prototype.start = function () {
33
+ var self = this;
34
+ var interval = this.config.statusPollInterval || 1000;
35
+
36
+ self.poll();
37
+ this.timer = setInterval(function () {
38
+ self.poll();
39
+ }, interval);
40
+ };
41
+
42
+ /** 停止轮询 */
43
+ StatusPoller.prototype.stop = function () {
44
+ if (this.timer) {
45
+ clearInterval(this.timer);
46
+ this.timer = null;
47
+ }
48
+ };
49
+
50
+ /** 重置 (每次重载前调用) */
51
+ StatusPoller.prototype.reset = function () {
52
+ this.lastStatus = null;
53
+ };
54
+
55
+ /** 单次轮询 */
56
+ StatusPoller.prototype.poll = function () {
57
+ var self = this;
58
+ self.hdc.readResult("hot-status.json", self.config).then(function (content) {
59
+ if (!content) return;
60
+ var status;
61
+ try { status = JSON.parse(content.trim()); } catch (e) { return; }
62
+ if (!status) return;
63
+
64
+ var statusKey = status.status + ":" + status.ts;
65
+ if (statusKey !== self.lastStatus) {
66
+ self.lastStatus = statusKey;
67
+ self.emit("status", status);
68
+ }
69
+ }).catch(function () {});
70
+ };
71
+
72
+ module.exports = StatusPoller;
package/src/utils.js ADDED
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * utils.js — 共享工具函数
4
+ */
5
+ var fs = require("fs");
6
+ var path = require("path");
7
+
8
+ /** 去除 UTF-8 BOM */
9
+ function stripBOM(content) {
10
+ if (typeof content === "string") {
11
+ if (content.charCodeAt(0) === 0xfeff) return content.slice(1);
12
+ return content;
13
+ }
14
+ if (Buffer.isBuffer(content)) {
15
+ if (content.length >= 3 && content[0] === 0xef && content[1] === 0xbb && content[2] === 0xbf) {
16
+ return content.slice(3).toString("utf8");
17
+ }
18
+ return content.toString("utf8");
19
+ }
20
+ return content;
21
+ }
22
+
23
+ /** 简易 JSON5 解析(处理注释、尾逗号) */
24
+ function parseJson5(content) {
25
+ var cleaned = content
26
+ .replace(/\/\/.*$/gm, "") // 单行注释
27
+ .replace(/\/\*[\s\S]*?\*\//g, "") // 多行注释
28
+ .replace(/,\s*([}\]])/g, "$1"); // 尾逗号
29
+ return JSON.parse(cleaned);
30
+ }
31
+
32
+ /** 读取并解析 JSON5 文件 */
33
+ function readJson5(filePath) {
34
+ var content = fs.readFileSync(filePath, "utf8");
35
+ return parseJson5(content);
36
+ }
37
+
38
+ /** 简易 hash(用于 diff,非加密) */
39
+ function simpleHash(str) {
40
+ var hash = 5381;
41
+ for (var i = 0; i < str.length; i++) {
42
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0x7fffffff;
43
+ }
44
+ return hash.toString(36);
45
+ }
46
+
47
+ /** 格式化毫秒 */
48
+ function fmtMs(ms) {
49
+ if (ms < 1000) return ms + "ms";
50
+ return (ms / 1000).toFixed(1) + "s";
51
+ }
52
+
53
+ /** ANSI 颜色 */
54
+ var c = {
55
+ reset: "\x1b[0m",
56
+ bold: "\x1b[1m",
57
+ dim: "\x1b[2m",
58
+ red: "\x1b[31m",
59
+ green: "\x1b[32m",
60
+ yellow: "\x1b[33m",
61
+ blue: "\x1b[34m",
62
+ magenta: "\x1b[35m",
63
+ cyan: "\x1b[36m",
64
+ gray: "\x1b[90m",
65
+ };
66
+
67
+ module.exports = {
68
+ stripBOM: stripBOM,
69
+ parseJson5: parseJson5,
70
+ readJson5: readJson5,
71
+ simpleHash: simpleHash,
72
+ fmtMs: fmtMs,
73
+ c: c,
74
+ };
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ /**
3
+ * hot-reload-agent.js — 设备侧热加载核心
4
+ *
5
+ * 通过 package.json 的 main 字段被 Electron 加载。
6
+ * 不修改开发者的 main.js,由本文件接管启动流程。
7
+ *
8
+ * 优先级:
9
+ * 1) /data/storage/el2/base/files/hot/entry.js (override, hdc 推送的开发者代码)
10
+ * 2) ./main.js (开发者的原始入口,生产回退)
11
+ */
12
+ var fs = require("fs");
13
+ var path = require("path");
14
+ var electron = require("electron");
15
+ var app = electron.app;
16
+
17
+ // ─── 路径常量 ───
18
+ var OVERRIDE_DIR = "/data/storage/el2/base/files/hot";
19
+ var ENTRY_JS = path.join(OVERRIDE_DIR, "entry.js");
20
+ var CONFIG_JSON = path.join(OVERRIDE_DIR, "hot-config.json");
21
+ var TRIGGER_FILE = path.join(OVERRIDE_DIR, ".reload-trigger");
22
+ var STATUS_FILE = "/data/storage/el2/base/files/hot-status.json";
23
+ var LOG_FILE = "/data/storage/el2/base/files/hot-harness.log";
24
+
25
+ // 开发者原始入口(与 package.json 原 main 字段一致)
26
+ var DEV_MAIN = path.join(__dirname, "main.js");
27
+
28
+ // ─── 运行时配置 ───
29
+ var config = {
30
+ strategy: "hard", // "soft" | "hard" | "hard-first"
31
+ softTimeout: 5000
32
+ };
33
+
34
+ var currentModule = null;
35
+ var reloadInProgress = false;
36
+
37
+ // ─── 日志 & 状态 ───
38
+ function log(msg) {
39
+ var line = new Date().toISOString() + " [agent] " + msg;
40
+ try { fs.appendFileSync(LOG_FILE, line + "\n"); } catch (e) {}
41
+ }
42
+
43
+ function writeStatus(status, extra) {
44
+ var s = Object.assign({
45
+ ts: Date.now(),
46
+ status: status,
47
+ pid: process.pid,
48
+ strategy: config.strategy
49
+ }, extra || {});
50
+ try { fs.writeFileSync(STATUS_FILE, JSON.stringify(s)); } catch (e) {}
51
+ }
52
+
53
+ function loadConfig() {
54
+ try {
55
+ var cfg = JSON.parse(fs.readFileSync(CONFIG_JSON, "utf8"));
56
+ if (cfg.strategy) config.strategy = cfg.strategy;
57
+ if (cfg.softTimeout) config.softTimeout = cfg.softTimeout;
58
+ log("config loaded: " + JSON.stringify(config));
59
+ } catch (e) {}
60
+ }
61
+
62
+ // ─── 加载开发者代码 ───
63
+ function loadEntry() {
64
+ // 优先加载 override 目录的 entry.js(热加载代码)
65
+ if (fs.existsSync(ENTRY_JS)) {
66
+ log("loading entry.js from override");
67
+ return require(ENTRY_JS);
68
+ }
69
+
70
+ // 回退:加载开发者的 main.js
71
+ log("loading dev main.js");
72
+ return require(DEV_MAIN);
73
+ }
74
+
75
+ // ─── 软重载 ───
76
+ function softReload() {
77
+ log("soft reload start");
78
+ writeStatus("soft-reloading");
79
+
80
+ try {
81
+ if (currentModule && typeof currentModule.cleanup === "function") {
82
+ try { currentModule.cleanup(); } catch (e) { log("cleanup error: " + e.message); }
83
+ }
84
+
85
+ // 失效 require.cache:override 目录 + 开发者入口
86
+ Object.keys(require.cache).forEach(function (key) {
87
+ if (key.indexOf(OVERRIDE_DIR) === 0 || key === DEV_MAIN) {
88
+ delete require.cache[key];
89
+ }
90
+ });
91
+
92
+ var mod = loadEntry();
93
+ currentModule = mod;
94
+ writeStatus("soft-ok", { loadTime: Date.now() });
95
+ log("soft reload done");
96
+ return true;
97
+ } catch (e) {
98
+ log("soft reload failed: " + (e.stack || e.message));
99
+ writeStatus("soft-fail", { error: e.message });
100
+ return false;
101
+ }
102
+ }
103
+
104
+ // ─── 硬重载 ───
105
+ function hardReload() {
106
+ // hard 模式由主机侧 force-stop + aa start 重启,agent 不主动 quit
107
+ log("hard reload: waiting for host to restart");
108
+ writeStatus("hard-reloading");
109
+ }
110
+
111
+ // ─── 重载调度 ───
112
+ function reload() {
113
+ if (reloadInProgress) {
114
+ log("reload already in progress, skipping");
115
+ return;
116
+ }
117
+ reloadInProgress = true;
118
+ loadConfig();
119
+
120
+ if (config.strategy === "hard") {
121
+ hardReload();
122
+ reloadInProgress = false;
123
+ return;
124
+ }
125
+
126
+ if (config.strategy === "soft") {
127
+ softReload();
128
+ reloadInProgress = false;
129
+ return;
130
+ }
131
+
132
+ // hard-first: 先 soft,失败后写状态等主机侧重启
133
+ var ok = softReload();
134
+ reloadInProgress = false;
135
+ if (!ok) {
136
+ log("soft failed, waiting for host to restart");
137
+ writeStatus("hard-reloading");
138
+ }
139
+ }
140
+
141
+ // ─── 文件监听 ───
142
+ function startWatch() {
143
+ try { fs.mkdirSync(OVERRIDE_DIR, { recursive: true }); } catch (e) {}
144
+
145
+ try {
146
+ fs.watch(OVERRIDE_DIR, { persistent: false }, function (eventType, filename) {
147
+ if (filename === ".reload-trigger" || filename === "entry.js") {
148
+ log("watch event: " + eventType + " " + filename);
149
+ reload();
150
+ }
151
+ });
152
+ log("fs.watch active on " + OVERRIDE_DIR);
153
+ } catch (e) {
154
+ log("fs.watch failed, polling only: " + e.message);
155
+ }
156
+
157
+ var lastTriggerMtime = 0;
158
+ setInterval(function () {
159
+ try {
160
+ var stat = fs.statSync(TRIGGER_FILE);
161
+ if (stat.mtimeMs !== lastTriggerMtime) {
162
+ lastTriggerMtime = stat.mtimeMs;
163
+ log("polling detected trigger change");
164
+ reload();
165
+ }
166
+ } catch (e) {}
167
+ }, 2000);
168
+ }
169
+
170
+ // ─── 启动 ───
171
+ app.whenReady().then(function () {
172
+ loadConfig();
173
+ writeStatus("agent-ready");
174
+ log("agent started, strategy=" + config.strategy);
175
+
176
+ try {
177
+ currentModule = loadEntry();
178
+ writeStatus("loaded");
179
+ log("first load done");
180
+ } catch (e) {
181
+ log("first load failed: " + (e.stack || e.message));
182
+ writeStatus("init-fail", { error: e.message });
183
+ }
184
+
185
+ startWatch();
186
+ });