@walkhi/code-relax 0.1.0-beta.68 → 0.1.0-beta.69
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.
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-harmony-release.mjs +46 -46
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/bin/set-relay-admin-password.mjs +11 -11
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/shared/app-server-events.cjs +511 -511
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-ice-policy.cjs +22 -22
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -69
- package/dist/shared/release-version.cjs +31 -31
- package/dist/src/archived-threads.mjs +22 -22
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +62 -62
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/lan-socket-server.mjs +1 -1
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +19 -19
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-permission-mode.mjs +17 -17
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/file-opener.mjs +9 -9
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -56
- package/dist/src/platform/linux/desktop-installation.mjs +36 -36
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/linux/network-info.mjs +11 -11
- package/dist/src/platform/linux/project-locations.mjs +17 -17
- package/dist/src/platform/linux/resident-startup.mjs +52 -52
- package/dist/src/platform/linux/retained-runtime.mjs +76 -76
- package/dist/src/platform/linux/service-host.mjs +18 -18
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/macos/network-info.mjs +8 -8
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -23
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/power-inhibitor.mjs +22 -22
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
- package/dist/src/platform/windows/keep-awake.ps1 +20 -20
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-create.mjs +121 -121
- package/dist/src/project-management.mjs +1 -1
- package/dist/src/project-remove.mjs +55 -55
- package/dist/src/project-update.mjs +59 -59
- package/dist/src/relay-config-import.mjs +45 -45
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-auth.mjs +59 -59
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-login-page.mjs +7 -7
- package/dist/src/self-relay/admin-server.mjs +105 -105
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
- package/dist/src/self-relay/admin-workstations.mjs +143 -143
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connection-reporting.mjs +38 -38
- package/dist/src/self-relay/connector.mjs +13 -2
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +6 -6
- package/dist/src/self-relay/device-info.mjs +27 -27
- package/dist/src/self-relay/device-store.mjs +92 -92
- package/dist/src/self-relay/lan-authorizations.mjs +71 -71
- package/dist/src/self-relay/lifecycle.mjs +278 -278
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -118
- package/dist/src/self-relay/phone-connections.mjs +55 -55
- package/dist/src/self-relay/server.mjs +14 -3
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +40 -18
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-power.mjs +58 -58
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/service-update-worker.mjs +42 -42
- package/dist/src/service-update.mjs +80 -80
- package/dist/src/shared-recovery.mjs +152 -152
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +4 -4
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
3
|
-
import { launchPowerInhibitor } from './platform/power-inhibitor.mjs';
|
|
4
|
-
|
|
5
|
-
export const powerConfigPath = stateRoot => path.join(stateRoot, 'keep-awake.json');
|
|
6
|
-
export const powerEnabled = stateRoot => readRecord(powerConfigPath(stateRoot))?.enabled !== false;
|
|
7
|
-
|
|
8
|
-
export function createPowerManager(stateRoot, { launch = launchPowerInhibitor, report = console.error } = {}) {
|
|
9
|
-
let enabled = powerEnabled(stateRoot), active = false, error = '', child = null, closed, stopping = false;
|
|
10
|
-
let queue = Promise.resolve();
|
|
11
|
-
const snapshot = () => ({ enabled, active, error });
|
|
12
|
-
async function release() {
|
|
13
|
-
if (!child) return;
|
|
14
|
-
stopping = true;
|
|
15
|
-
child.stdin.end();
|
|
16
|
-
await closed;
|
|
17
|
-
}
|
|
18
|
-
async function acquire() {
|
|
19
|
-
if (child || !enabled) return;
|
|
20
|
-
error = '';
|
|
21
|
-
try {
|
|
22
|
-
const process = launch();
|
|
23
|
-
stopping = false;
|
|
24
|
-
child = process;
|
|
25
|
-
let stderr = '', output = '';
|
|
26
|
-
process.stderr.on('data', data => { stderr = (stderr + data).slice(-2048); });
|
|
27
|
-
process.stdin.on('error', cause => { if (cause.code !== 'EPIPE') report(`保持唤醒通道异常:${cause.message}`); });
|
|
28
|
-
closed = new Promise(resolve => process.once('close', code => {
|
|
29
|
-
if (active && !stopping) { error = stderr.trim() || `保持唤醒进程意外退出(${code})`; report(error); }
|
|
30
|
-
child = null; active = false; resolve();
|
|
31
|
-
}));
|
|
32
|
-
await new Promise((resolve, reject) => {
|
|
33
|
-
const timer = setTimeout(() => { process.kill(); reject(new Error('保持唤醒申请超时')); }, 8000);
|
|
34
|
-
const finish = cause => { clearTimeout(timer); cause ? reject(cause) : resolve(); };
|
|
35
|
-
process.once('error', finish);
|
|
36
|
-
process.once('close', code => {
|
|
37
|
-
finish(new Error(stderr.trim() || `保持唤醒进程已退出(${code})`));
|
|
38
|
-
});
|
|
39
|
-
process.stdout.on('data', data => {
|
|
40
|
-
output += data;
|
|
41
|
-
if (output.includes('ready\n') || output.includes('ready\r\n')) { active = true; finish(); }
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
} catch (cause) { error = cause.message; report(`保持唤醒未生效:${error}`); }
|
|
45
|
-
}
|
|
46
|
-
const serial = task => { const result = queue.then(task); queue = result.catch(() => {}); return result; };
|
|
47
|
-
return {
|
|
48
|
-
snapshot,
|
|
49
|
-
start: () => serial(async () => { await acquire(); return snapshot(); }),
|
|
50
|
-
setEnabled: value => serial(async () => {
|
|
51
|
-
writeRecord(powerConfigPath(stateRoot), { enabled: value });
|
|
52
|
-
enabled = value;
|
|
53
|
-
if (enabled) await acquire(); else { await release(); error = ''; }
|
|
54
|
-
return snapshot();
|
|
55
|
-
}),
|
|
56
|
-
close: () => serial(async () => { enabled = false; await release(); }),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
3
|
+
import { launchPowerInhibitor } from './platform/power-inhibitor.mjs';
|
|
4
|
+
|
|
5
|
+
export const powerConfigPath = stateRoot => path.join(stateRoot, 'keep-awake.json');
|
|
6
|
+
export const powerEnabled = stateRoot => readRecord(powerConfigPath(stateRoot))?.enabled !== false;
|
|
7
|
+
|
|
8
|
+
export function createPowerManager(stateRoot, { launch = launchPowerInhibitor, report = console.error } = {}) {
|
|
9
|
+
let enabled = powerEnabled(stateRoot), active = false, error = '', child = null, closed, stopping = false;
|
|
10
|
+
let queue = Promise.resolve();
|
|
11
|
+
const snapshot = () => ({ enabled, active, error });
|
|
12
|
+
async function release() {
|
|
13
|
+
if (!child) return;
|
|
14
|
+
stopping = true;
|
|
15
|
+
child.stdin.end();
|
|
16
|
+
await closed;
|
|
17
|
+
}
|
|
18
|
+
async function acquire() {
|
|
19
|
+
if (child || !enabled) return;
|
|
20
|
+
error = '';
|
|
21
|
+
try {
|
|
22
|
+
const process = launch();
|
|
23
|
+
stopping = false;
|
|
24
|
+
child = process;
|
|
25
|
+
let stderr = '', output = '';
|
|
26
|
+
process.stderr.on('data', data => { stderr = (stderr + data).slice(-2048); });
|
|
27
|
+
process.stdin.on('error', cause => { if (cause.code !== 'EPIPE') report(`保持唤醒通道异常:${cause.message}`); });
|
|
28
|
+
closed = new Promise(resolve => process.once('close', code => {
|
|
29
|
+
if (active && !stopping) { error = stderr.trim() || `保持唤醒进程意外退出(${code})`; report(error); }
|
|
30
|
+
child = null; active = false; resolve();
|
|
31
|
+
}));
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
const timer = setTimeout(() => { process.kill(); reject(new Error('保持唤醒申请超时')); }, 8000);
|
|
34
|
+
const finish = cause => { clearTimeout(timer); cause ? reject(cause) : resolve(); };
|
|
35
|
+
process.once('error', finish);
|
|
36
|
+
process.once('close', code => {
|
|
37
|
+
finish(new Error(stderr.trim() || `保持唤醒进程已退出(${code})`));
|
|
38
|
+
});
|
|
39
|
+
process.stdout.on('data', data => {
|
|
40
|
+
output += data;
|
|
41
|
+
if (output.includes('ready\n') || output.includes('ready\r\n')) { active = true; finish(); }
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
} catch (cause) { error = cause.message; report(`保持唤醒未生效:${error}`); }
|
|
45
|
+
}
|
|
46
|
+
const serial = task => { const result = queue.then(task); queue = result.catch(() => {}); return result; };
|
|
47
|
+
return {
|
|
48
|
+
snapshot,
|
|
49
|
+
start: () => serial(async () => { await acquire(); return snapshot(); }),
|
|
50
|
+
setEnabled: value => serial(async () => {
|
|
51
|
+
writeRecord(powerConfigPath(stateRoot), { enabled: value });
|
|
52
|
+
enabled = value;
|
|
53
|
+
if (enabled) await acquire(); else { await release(); error = ''; }
|
|
54
|
+
return snapshot();
|
|
55
|
+
}),
|
|
56
|
+
close: () => serial(async () => { enabled = false; await release(); }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
-
import { startBackground } from './background-process.mjs';
|
|
7
|
-
import { lifecycle, readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
8
|
-
import * as host from './platform/service-host.mjs';
|
|
9
|
-
import { desktopHost } from './platform/desktop-host.mjs';
|
|
10
|
-
|
|
11
|
-
const script = fileURLToPath(import.meta.url);
|
|
12
|
-
const sameProcess = (expected, actual) => expected && actual && expected.pid === actual.pid &&
|
|
13
|
-
expected.created === actual.created && expected.executable === actual.executable;
|
|
14
|
-
async function processIdentity(pid) {
|
|
15
|
-
const current = await desktopHost.process(pid);
|
|
16
|
-
return current ? { pid: current.pid, created: current.created, executable: current.executable } : null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function requestServiceRestart(stateRoot) {
|
|
20
|
-
const lock = path.join(stateRoot, 'service-restart.lock');
|
|
21
|
-
const operationId = randomUUID();
|
|
22
|
-
const owner = await processIdentity(process.pid);
|
|
23
|
-
if (!owner) throw new Error('无法确认桌面服务重启请求进程。');
|
|
24
|
-
const previous = readRecord(lock);
|
|
25
|
-
if (previous) {
|
|
26
|
-
const actual = previous.owner?.pid ? await processIdentity(previous.owner.pid) : null;
|
|
27
|
-
if (sameProcess(previous.owner, actual) || readRecord(lock)?.operationId !== previous.operationId) {
|
|
28
|
-
throw new Error('桌面服务已在重启中。');
|
|
29
|
-
}
|
|
30
|
-
fs.rmSync(lock);
|
|
31
|
-
}
|
|
32
|
-
const temporary = `${lock}.${operationId}.tmp`;
|
|
33
|
-
try {
|
|
34
|
-
fs.writeFileSync(temporary, JSON.stringify({ operationId, owner, requestedAt: new Date().toISOString() }),
|
|
35
|
-
{ flag: 'wx', mode: 0o600 });
|
|
36
|
-
// Publish a complete record exclusively; a crash cannot leave a half-written lock.
|
|
37
|
-
fs.linkSync(temporary, lock);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
if (error.code === 'EEXIST') throw new Error('桌面服务已在重启中。');
|
|
40
|
-
throw error;
|
|
41
|
-
} finally { fs.rmSync(temporary, { force: true }); }
|
|
42
|
-
try {
|
|
43
|
-
const log = path.join(stateRoot, 'service-restart.log');
|
|
44
|
-
const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, operationId], process.env, [log, log]);
|
|
45
|
-
const worker = await processIdentity(pid);
|
|
46
|
-
if (!worker) throw new Error('桌面服务重启进程未启动。');
|
|
47
|
-
const current = readRecord(lock);
|
|
48
|
-
if (current?.operationId !== operationId) throw new Error('桌面服务重启实例已失效。');
|
|
49
|
-
writeRecord(lock, { ...current, owner: worker });
|
|
50
|
-
return { operationId, status: 'scheduled', message: '桌面服务重启已安排。' };
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const current = readRecord(lock);
|
|
53
|
-
if (current?.operationId === operationId && sameProcess(current.owner, owner)) fs.rmSync(lock, { force: true });
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function run(stateRoot, operationId) {
|
|
59
|
-
const lock = path.join(stateRoot, 'service-restart.lock');
|
|
60
|
-
try {
|
|
61
|
-
const deadline = Date.now() + 30000;
|
|
62
|
-
while (true) {
|
|
63
|
-
const record = readRecord(lock);
|
|
64
|
-
if (record?.operationId !== operationId) throw new Error('桌面服务重启实例已失效。');
|
|
65
|
-
if (record.owner?.pid === process.pid) break;
|
|
66
|
-
if (Date.now() >= deadline) throw new Error('桌面服务重启进程交接超时。');
|
|
67
|
-
await delay(100);
|
|
68
|
-
}
|
|
69
|
-
const service = lifecycle({ stateRoot, host });
|
|
70
|
-
await delay(1000);
|
|
71
|
-
await service.stop();
|
|
72
|
-
await service.start();
|
|
73
|
-
} finally {
|
|
74
|
-
if (readRecord(lock)?.operationId === operationId) fs.rmSync(lock, { force: true });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
|
|
79
|
-
const [stateRoot, operationId] = process.argv.slice(3);
|
|
80
|
-
run(stateRoot, operationId).catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
81
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { startBackground } from './background-process.mjs';
|
|
7
|
+
import { lifecycle, readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
8
|
+
import * as host from './platform/service-host.mjs';
|
|
9
|
+
import { desktopHost } from './platform/desktop-host.mjs';
|
|
10
|
+
|
|
11
|
+
const script = fileURLToPath(import.meta.url);
|
|
12
|
+
const sameProcess = (expected, actual) => expected && actual && expected.pid === actual.pid &&
|
|
13
|
+
expected.created === actual.created && expected.executable === actual.executable;
|
|
14
|
+
async function processIdentity(pid) {
|
|
15
|
+
const current = await desktopHost.process(pid);
|
|
16
|
+
return current ? { pid: current.pid, created: current.created, executable: current.executable } : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function requestServiceRestart(stateRoot) {
|
|
20
|
+
const lock = path.join(stateRoot, 'service-restart.lock');
|
|
21
|
+
const operationId = randomUUID();
|
|
22
|
+
const owner = await processIdentity(process.pid);
|
|
23
|
+
if (!owner) throw new Error('无法确认桌面服务重启请求进程。');
|
|
24
|
+
const previous = readRecord(lock);
|
|
25
|
+
if (previous) {
|
|
26
|
+
const actual = previous.owner?.pid ? await processIdentity(previous.owner.pid) : null;
|
|
27
|
+
if (sameProcess(previous.owner, actual) || readRecord(lock)?.operationId !== previous.operationId) {
|
|
28
|
+
throw new Error('桌面服务已在重启中。');
|
|
29
|
+
}
|
|
30
|
+
fs.rmSync(lock);
|
|
31
|
+
}
|
|
32
|
+
const temporary = `${lock}.${operationId}.tmp`;
|
|
33
|
+
try {
|
|
34
|
+
fs.writeFileSync(temporary, JSON.stringify({ operationId, owner, requestedAt: new Date().toISOString() }),
|
|
35
|
+
{ flag: 'wx', mode: 0o600 });
|
|
36
|
+
// Publish a complete record exclusively; a crash cannot leave a half-written lock.
|
|
37
|
+
fs.linkSync(temporary, lock);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code === 'EEXIST') throw new Error('桌面服务已在重启中。');
|
|
40
|
+
throw error;
|
|
41
|
+
} finally { fs.rmSync(temporary, { force: true }); }
|
|
42
|
+
try {
|
|
43
|
+
const log = path.join(stateRoot, 'service-restart.log');
|
|
44
|
+
const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, operationId], process.env, [log, log]);
|
|
45
|
+
const worker = await processIdentity(pid);
|
|
46
|
+
if (!worker) throw new Error('桌面服务重启进程未启动。');
|
|
47
|
+
const current = readRecord(lock);
|
|
48
|
+
if (current?.operationId !== operationId) throw new Error('桌面服务重启实例已失效。');
|
|
49
|
+
writeRecord(lock, { ...current, owner: worker });
|
|
50
|
+
return { operationId, status: 'scheduled', message: '桌面服务重启已安排。' };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const current = readRecord(lock);
|
|
53
|
+
if (current?.operationId === operationId && sameProcess(current.owner, owner)) fs.rmSync(lock, { force: true });
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function run(stateRoot, operationId) {
|
|
59
|
+
const lock = path.join(stateRoot, 'service-restart.lock');
|
|
60
|
+
try {
|
|
61
|
+
const deadline = Date.now() + 30000;
|
|
62
|
+
while (true) {
|
|
63
|
+
const record = readRecord(lock);
|
|
64
|
+
if (record?.operationId !== operationId) throw new Error('桌面服务重启实例已失效。');
|
|
65
|
+
if (record.owner?.pid === process.pid) break;
|
|
66
|
+
if (Date.now() >= deadline) throw new Error('桌面服务重启进程交接超时。');
|
|
67
|
+
await delay(100);
|
|
68
|
+
}
|
|
69
|
+
const service = lifecycle({ stateRoot, host });
|
|
70
|
+
await delay(1000);
|
|
71
|
+
await service.stop();
|
|
72
|
+
await service.start();
|
|
73
|
+
} finally {
|
|
74
|
+
if (readRecord(lock)?.operationId === operationId) fs.rmSync(lock, { force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
|
|
79
|
+
const [stateRoot, operationId] = process.argv.slice(3);
|
|
80
|
+
run(stateRoot, operationId).catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
81
|
+
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
-
|
|
6
|
-
// Copied outside both npm and service directories before launch. Builtins only.
|
|
7
|
-
const jobFile = process.argv[2];
|
|
8
|
-
const job = JSON.parse(fs.readFileSync(jobFile, 'utf8'));
|
|
9
|
-
process.chdir(path.dirname(jobFile));
|
|
10
|
-
const read = () => JSON.parse(fs.readFileSync(job.statusFile, 'utf8'));
|
|
11
|
-
function update(values) {
|
|
12
|
-
const current = read();
|
|
13
|
-
if (current.operationId !== job.operationId) throw new Error('更新操作已被替换');
|
|
14
|
-
const temporary = `${job.statusFile}.${process.pid}.tmp`;
|
|
15
|
-
fs.writeFileSync(temporary, JSON.stringify({ ...current, ...values }), { mode: 0o600 });
|
|
16
|
-
fs.renameSync(temporary, job.statusFile);
|
|
17
|
-
}
|
|
18
|
-
try {
|
|
19
|
-
const deadline = Date.now() + 30000;
|
|
20
|
-
while (read().owner?.pid !== process.pid) {
|
|
21
|
-
if (Date.now() >= deadline) throw new Error('独立更新进程交接超时');
|
|
22
|
-
await delay(100);
|
|
23
|
-
}
|
|
24
|
-
await delay(1500);
|
|
25
|
-
update({ status: 'running', startedAt: new Date().toISOString() });
|
|
26
|
-
const args = [job.npmCliPath, 'install', '--global', `@walkhi/code-relax@${job.targetVersion}`,
|
|
27
|
-
'--foreground-scripts', '--allow-scripts=@walkhi/code-relax,node-datachannel,sharp', '--registry=https://registry.npmjs.org'];
|
|
28
|
-
if (job.npmPrefix) args.push('--prefix', job.npmPrefix);
|
|
29
|
-
const child = spawn(job.nodeExecutable, args, { cwd: path.dirname(jobFile), windowsHide: true,
|
|
30
|
-
stdio: ['ignore', 'inherit', 'inherit'] });
|
|
31
|
-
const code = await new Promise((resolve, reject) => {
|
|
32
|
-
child.once('error', reject); child.once('exit', (code, signal) => resolve(code ?? `signal ${signal}`));
|
|
33
|
-
});
|
|
34
|
-
if (code !== 0) throw new Error(`npm 安装失败(${code}),请查看桌面服务更新日志。`);
|
|
35
|
-
const installed = JSON.parse(fs.readFileSync(path.join(job.serviceRoot, 'installation.json'), 'utf8'));
|
|
36
|
-
if (installed.version !== job.targetVersion) throw new Error('安装结束,但桌面服务版本与目标版本不一致。');
|
|
37
|
-
update({ status: 'completed', finishedAt: new Date().toISOString(), installedVersion: installed.version });
|
|
38
|
-
} catch (error) {
|
|
39
|
-
console.error(error.stack || error.message);
|
|
40
|
-
update({ status: 'failed', finishedAt: new Date().toISOString(), error: error.message });
|
|
41
|
-
process.exitCode = 1;
|
|
42
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
|
|
6
|
+
// Copied outside both npm and service directories before launch. Builtins only.
|
|
7
|
+
const jobFile = process.argv[2];
|
|
8
|
+
const job = JSON.parse(fs.readFileSync(jobFile, 'utf8'));
|
|
9
|
+
process.chdir(path.dirname(jobFile));
|
|
10
|
+
const read = () => JSON.parse(fs.readFileSync(job.statusFile, 'utf8'));
|
|
11
|
+
function update(values) {
|
|
12
|
+
const current = read();
|
|
13
|
+
if (current.operationId !== job.operationId) throw new Error('更新操作已被替换');
|
|
14
|
+
const temporary = `${job.statusFile}.${process.pid}.tmp`;
|
|
15
|
+
fs.writeFileSync(temporary, JSON.stringify({ ...current, ...values }), { mode: 0o600 });
|
|
16
|
+
fs.renameSync(temporary, job.statusFile);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const deadline = Date.now() + 30000;
|
|
20
|
+
while (read().owner?.pid !== process.pid) {
|
|
21
|
+
if (Date.now() >= deadline) throw new Error('独立更新进程交接超时');
|
|
22
|
+
await delay(100);
|
|
23
|
+
}
|
|
24
|
+
await delay(1500);
|
|
25
|
+
update({ status: 'running', startedAt: new Date().toISOString() });
|
|
26
|
+
const args = [job.npmCliPath, 'install', '--global', `@walkhi/code-relax@${job.targetVersion}`,
|
|
27
|
+
'--foreground-scripts', '--allow-scripts=@walkhi/code-relax,node-datachannel,sharp', '--registry=https://registry.npmjs.org'];
|
|
28
|
+
if (job.npmPrefix) args.push('--prefix', job.npmPrefix);
|
|
29
|
+
const child = spawn(job.nodeExecutable, args, { cwd: path.dirname(jobFile), windowsHide: true,
|
|
30
|
+
stdio: ['ignore', 'inherit', 'inherit'] });
|
|
31
|
+
const code = await new Promise((resolve, reject) => {
|
|
32
|
+
child.once('error', reject); child.once('exit', (code, signal) => resolve(code ?? `signal ${signal}`));
|
|
33
|
+
});
|
|
34
|
+
if (code !== 0) throw new Error(`npm 安装失败(${code}),请查看桌面服务更新日志。`);
|
|
35
|
+
const installed = JSON.parse(fs.readFileSync(path.join(job.serviceRoot, 'installation.json'), 'utf8'));
|
|
36
|
+
if (installed.version !== job.targetVersion) throw new Error('安装结束,但桌面服务版本与目标版本不一致。');
|
|
37
|
+
update({ status: 'completed', finishedAt: new Date().toISOString(), installedVersion: installed.version });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(error.stack || error.message);
|
|
40
|
+
update({ status: 'failed', finishedAt: new Date().toISOString(), error: error.message });
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { startBackground } from './background-process.mjs';
|
|
5
|
-
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
6
|
-
import { runtimePaths } from './platform/runtime-paths.mjs';
|
|
7
|
-
import { desktopHost } from './platform/desktop-host.mjs';
|
|
8
|
-
import releaseVersion from '../shared/release-version.cjs';
|
|
9
|
-
|
|
10
|
-
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
11
|
-
const statusPath = root => path.join(root, 'service-update.json');
|
|
12
|
-
const installation = () => readRecord(path.join(runtimePaths().serviceRoot, 'installation.json'));
|
|
13
|
-
export function publicUpdateOperation(record, installedVersion) {
|
|
14
|
-
if (!record) return null;
|
|
15
|
-
return { operationId: record.operationId, targetVersion: record.targetVersion,
|
|
16
|
-
status: record.status, targetInstalled: Boolean(installedVersion && installedVersion === record.targetVersion),
|
|
17
|
-
error: record.status === 'failed' ? '上次更新未完整完成,请查看桌面服务更新日志。' : null,
|
|
18
|
-
installedVersion: record.installedVersion || null };
|
|
19
|
-
}
|
|
20
|
-
export function serviceUpdateStatus(root) {
|
|
21
|
-
const record = readRecord(statusPath(root));
|
|
22
|
-
const installed = installation();
|
|
23
|
-
return { supported: Boolean(installed?.npmCliPath && fs.existsSync(installed.npmCliPath)),
|
|
24
|
-
currentVersion: installed?.version || null,
|
|
25
|
-
operation: publicUpdateOperation(record, installed?.version) };
|
|
26
|
-
}
|
|
27
|
-
export async function checkServiceUpdate(root) {
|
|
28
|
-
const result = await fetch('https://registry.npmjs.org/@walkhi%2fcode-relax/latest', { signal: AbortSignal.timeout(12000) });
|
|
29
|
-
if (!result.ok) throw new Error(`查询桌面服务版本失败:HTTP ${result.status}`);
|
|
30
|
-
const latest = await result.json();
|
|
31
|
-
if (!versionPattern.test(latest.version || '') || releaseVersion.compare(latest.version, latest.version) !== 0) throw new Error('npm 返回的版本号无效');
|
|
32
|
-
const status = serviceUpdateStatus(root);
|
|
33
|
-
return { ...status, latestVersion: latest.version,
|
|
34
|
-
updateAvailable: releaseVersion.compare(latest.version, status.currentVersion) > 0 };
|
|
35
|
-
}
|
|
36
|
-
let scheduling = false;
|
|
37
|
-
export async function requestServiceUpdate(root, body) {
|
|
38
|
-
if (body.confirmed !== true || !/^[0-9a-f-]{36}$/i.test(body.operationId || '') || !versionPattern.test(body.targetVersion || '')) {
|
|
39
|
-
throw new Error('请确认桌面服务更新及目标版本后再继续。');
|
|
40
|
-
}
|
|
41
|
-
if (scheduling) throw new Error('桌面服务更新正在安排。');
|
|
42
|
-
scheduling = true;
|
|
43
|
-
try {
|
|
44
|
-
const previous = readRecord(statusPath(root));
|
|
45
|
-
if (previous?.operationId === body.operationId) return serviceUpdateStatus(root);
|
|
46
|
-
if (previous && ['scheduled', 'running'].includes(previous.status)) {
|
|
47
|
-
const actual = previous.owner?.pid ? await desktopHost.process(previous.owner.pid) : null;
|
|
48
|
-
if (actual && actual.created === previous.owner.created && actual.executable === previous.owner.executable) {
|
|
49
|
-
throw new Error('桌面服务已有更新正在执行。');
|
|
50
|
-
}
|
|
51
|
-
writeRecord(statusPath(root), { ...previous, status: 'failed', error: '更新进程已退出,未确认安装完成。' });
|
|
52
|
-
}
|
|
53
|
-
const latest = await checkServiceUpdate(root);
|
|
54
|
-
if (!latest.supported) throw new Error('当前安装缺少 npm 路径,请先安装支持手机更新的过渡版。');
|
|
55
|
-
if (latest.latestVersion !== body.targetVersion) throw new Error('最新版本已变化,请重新检查后确认。');
|
|
56
|
-
if (!latest.updateAvailable) throw new Error('没有高于当前安装版本的桌面服务更新。');
|
|
57
|
-
const installed = installation();
|
|
58
|
-
const directory = path.join(root, 'service-updates', body.operationId);
|
|
59
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
60
|
-
const worker = path.join(directory, 'worker.mjs');
|
|
61
|
-
fs.copyFileSync(fileURLToPath(new URL('./service-update-worker.mjs', import.meta.url)), worker);
|
|
62
|
-
const jobFile = path.join(directory, 'job.json');
|
|
63
|
-
writeRecord(jobFile, { ...body, statusFile: statusPath(root), npmCliPath: installed.npmCliPath,
|
|
64
|
-
npmPrefix: installed.npmPrefix, nodeExecutable: installed.nodeExecutable, serviceRoot: runtimePaths().serviceRoot });
|
|
65
|
-
writeRecord(statusPath(root), { operationId: body.operationId, targetVersion: body.targetVersion,
|
|
66
|
-
status: 'scheduled', requestedAt: new Date().toISOString() });
|
|
67
|
-
try {
|
|
68
|
-
const log = path.join(directory, 'update.log');
|
|
69
|
-
const pid = await startBackground(installed.nodeExecutable, [worker, jobFile], process.env, [log, log]);
|
|
70
|
-
const owner = await desktopHost.process(pid);
|
|
71
|
-
if (!owner) throw new Error('独立更新进程未启动。');
|
|
72
|
-
writeRecord(statusPath(root), { ...readRecord(statusPath(root)), owner: {
|
|
73
|
-
pid: owner.pid, created: owner.created, executable: owner.executable } });
|
|
74
|
-
} catch (error) {
|
|
75
|
-
writeRecord(statusPath(root), { ...readRecord(statusPath(root)), status: 'failed', error: error.message });
|
|
76
|
-
throw new Error('桌面服务更新启动未完成,请查看桌面服务更新日志;结果确认前不要重复提交。');
|
|
77
|
-
}
|
|
78
|
-
return serviceUpdateStatus(root);
|
|
79
|
-
} finally { scheduling = false; }
|
|
80
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { startBackground } from './background-process.mjs';
|
|
5
|
+
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
6
|
+
import { runtimePaths } from './platform/runtime-paths.mjs';
|
|
7
|
+
import { desktopHost } from './platform/desktop-host.mjs';
|
|
8
|
+
import releaseVersion from '../shared/release-version.cjs';
|
|
9
|
+
|
|
10
|
+
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
11
|
+
const statusPath = root => path.join(root, 'service-update.json');
|
|
12
|
+
const installation = () => readRecord(path.join(runtimePaths().serviceRoot, 'installation.json'));
|
|
13
|
+
export function publicUpdateOperation(record, installedVersion) {
|
|
14
|
+
if (!record) return null;
|
|
15
|
+
return { operationId: record.operationId, targetVersion: record.targetVersion,
|
|
16
|
+
status: record.status, targetInstalled: Boolean(installedVersion && installedVersion === record.targetVersion),
|
|
17
|
+
error: record.status === 'failed' ? '上次更新未完整完成,请查看桌面服务更新日志。' : null,
|
|
18
|
+
installedVersion: record.installedVersion || null };
|
|
19
|
+
}
|
|
20
|
+
export function serviceUpdateStatus(root) {
|
|
21
|
+
const record = readRecord(statusPath(root));
|
|
22
|
+
const installed = installation();
|
|
23
|
+
return { supported: Boolean(installed?.npmCliPath && fs.existsSync(installed.npmCliPath)),
|
|
24
|
+
currentVersion: installed?.version || null,
|
|
25
|
+
operation: publicUpdateOperation(record, installed?.version) };
|
|
26
|
+
}
|
|
27
|
+
export async function checkServiceUpdate(root) {
|
|
28
|
+
const result = await fetch('https://registry.npmjs.org/@walkhi%2fcode-relax/latest', { signal: AbortSignal.timeout(12000) });
|
|
29
|
+
if (!result.ok) throw new Error(`查询桌面服务版本失败:HTTP ${result.status}`);
|
|
30
|
+
const latest = await result.json();
|
|
31
|
+
if (!versionPattern.test(latest.version || '') || releaseVersion.compare(latest.version, latest.version) !== 0) throw new Error('npm 返回的版本号无效');
|
|
32
|
+
const status = serviceUpdateStatus(root);
|
|
33
|
+
return { ...status, latestVersion: latest.version,
|
|
34
|
+
updateAvailable: releaseVersion.compare(latest.version, status.currentVersion) > 0 };
|
|
35
|
+
}
|
|
36
|
+
let scheduling = false;
|
|
37
|
+
export async function requestServiceUpdate(root, body) {
|
|
38
|
+
if (body.confirmed !== true || !/^[0-9a-f-]{36}$/i.test(body.operationId || '') || !versionPattern.test(body.targetVersion || '')) {
|
|
39
|
+
throw new Error('请确认桌面服务更新及目标版本后再继续。');
|
|
40
|
+
}
|
|
41
|
+
if (scheduling) throw new Error('桌面服务更新正在安排。');
|
|
42
|
+
scheduling = true;
|
|
43
|
+
try {
|
|
44
|
+
const previous = readRecord(statusPath(root));
|
|
45
|
+
if (previous?.operationId === body.operationId) return serviceUpdateStatus(root);
|
|
46
|
+
if (previous && ['scheduled', 'running'].includes(previous.status)) {
|
|
47
|
+
const actual = previous.owner?.pid ? await desktopHost.process(previous.owner.pid) : null;
|
|
48
|
+
if (actual && actual.created === previous.owner.created && actual.executable === previous.owner.executable) {
|
|
49
|
+
throw new Error('桌面服务已有更新正在执行。');
|
|
50
|
+
}
|
|
51
|
+
writeRecord(statusPath(root), { ...previous, status: 'failed', error: '更新进程已退出,未确认安装完成。' });
|
|
52
|
+
}
|
|
53
|
+
const latest = await checkServiceUpdate(root);
|
|
54
|
+
if (!latest.supported) throw new Error('当前安装缺少 npm 路径,请先安装支持手机更新的过渡版。');
|
|
55
|
+
if (latest.latestVersion !== body.targetVersion) throw new Error('最新版本已变化,请重新检查后确认。');
|
|
56
|
+
if (!latest.updateAvailable) throw new Error('没有高于当前安装版本的桌面服务更新。');
|
|
57
|
+
const installed = installation();
|
|
58
|
+
const directory = path.join(root, 'service-updates', body.operationId);
|
|
59
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
60
|
+
const worker = path.join(directory, 'worker.mjs');
|
|
61
|
+
fs.copyFileSync(fileURLToPath(new URL('./service-update-worker.mjs', import.meta.url)), worker);
|
|
62
|
+
const jobFile = path.join(directory, 'job.json');
|
|
63
|
+
writeRecord(jobFile, { ...body, statusFile: statusPath(root), npmCliPath: installed.npmCliPath,
|
|
64
|
+
npmPrefix: installed.npmPrefix, nodeExecutable: installed.nodeExecutable, serviceRoot: runtimePaths().serviceRoot });
|
|
65
|
+
writeRecord(statusPath(root), { operationId: body.operationId, targetVersion: body.targetVersion,
|
|
66
|
+
status: 'scheduled', requestedAt: new Date().toISOString() });
|
|
67
|
+
try {
|
|
68
|
+
const log = path.join(directory, 'update.log');
|
|
69
|
+
const pid = await startBackground(installed.nodeExecutable, [worker, jobFile], process.env, [log, log]);
|
|
70
|
+
const owner = await desktopHost.process(pid);
|
|
71
|
+
if (!owner) throw new Error('独立更新进程未启动。');
|
|
72
|
+
writeRecord(statusPath(root), { ...readRecord(statusPath(root)), owner: {
|
|
73
|
+
pid: owner.pid, created: owner.created, executable: owner.executable } });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
writeRecord(statusPath(root), { ...readRecord(statusPath(root)), status: 'failed', error: error.message });
|
|
76
|
+
throw new Error('桌面服务更新启动未完成,请查看桌面服务更新日志;结果确认前不要重复提交。');
|
|
77
|
+
}
|
|
78
|
+
return serviceUpdateStatus(root);
|
|
79
|
+
} finally { scheduling = false; }
|
|
80
|
+
}
|