dominds 1.25.18 → 1.25.19
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/bootstrap/rtws-cli.d.ts +0 -1
- package/dist/bootstrap/rtws-cli.js +9 -3
- package/dist/cli/create.d.ts +1 -1
- package/dist/cli/create.js +1 -1
- package/dist/cli/read.js +1 -1
- package/dist/cli/tui.js +1 -1
- package/dist/cli/webui.js +1 -1
- package/dist/cli.js +6 -13
- package/dist/docs/cli-usage.md +7 -7
- package/dist/docs/cli-usage.zh.md +7 -7
- package/dist/docs/dialog-persistence.md +1 -1
- package/dist/docs/dominds-terminology.md +2 -2
- package/dist/server/dominds-self-update-restart-helper.d.ts +15 -0
- package/dist/server/dominds-self-update-restart-helper.js +305 -0
- package/dist/server/dominds-self-update.js +162 -120
- package/dist/server.js +4 -3
- package/package.json +1 -1
|
@@ -38,6 +38,12 @@ const path = __importStar(require("path"));
|
|
|
38
38
|
function extractGlobalRtwsChdir(params) {
|
|
39
39
|
let chdir;
|
|
40
40
|
const out = [];
|
|
41
|
+
const readAbsoluteChdir = (argName, value) => {
|
|
42
|
+
if (!path.isAbsolute(value)) {
|
|
43
|
+
throw new Error(`${argName} requires an absolute directory path: ${value}`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
};
|
|
41
47
|
for (let i = 0; i < params.argv.length; i++) {
|
|
42
48
|
const arg = params.argv[i];
|
|
43
49
|
if (arg === '--') {
|
|
@@ -49,7 +55,7 @@ function extractGlobalRtwsChdir(params) {
|
|
|
49
55
|
if (typeof next !== 'string' || next.length === 0 || next === '--') {
|
|
50
56
|
throw new Error(`${arg} requires a directory argument`);
|
|
51
57
|
}
|
|
52
|
-
chdir =
|
|
58
|
+
chdir = readAbsoluteChdir(arg, next);
|
|
53
59
|
i++;
|
|
54
60
|
continue;
|
|
55
61
|
}
|
|
@@ -57,14 +63,14 @@ function extractGlobalRtwsChdir(params) {
|
|
|
57
63
|
const value = arg.slice('--cwd='.length);
|
|
58
64
|
if (value.length === 0)
|
|
59
65
|
throw new Error(`--cwd requires a directory argument`);
|
|
60
|
-
chdir =
|
|
66
|
+
chdir = readAbsoluteChdir('--cwd', value);
|
|
61
67
|
continue;
|
|
62
68
|
}
|
|
63
69
|
if (arg.startsWith('--chdir=')) {
|
|
64
70
|
const value = arg.slice('--chdir='.length);
|
|
65
71
|
if (value.length === 0)
|
|
66
72
|
throw new Error(`--chdir requires a directory argument`);
|
|
67
|
-
chdir =
|
|
73
|
+
chdir = readAbsoluteChdir('--chdir', value);
|
|
68
74
|
continue;
|
|
69
75
|
}
|
|
70
76
|
out.push(arg);
|
package/dist/cli/create.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Notes:
|
|
10
10
|
* - Template can be a short name (resolved via DOMINDS_TEMPLATE_BASE) or a git URL.
|
|
11
|
-
* - rtws directory is `process.cwd()`. Use 'dominds -C <dir> create ...' to create under another base dir.
|
|
11
|
+
* - rtws directory is `process.cwd()`. Use 'dominds -C <abs-dir> create ...' to create under another base dir.
|
|
12
12
|
*/
|
|
13
13
|
declare function main(argv?: readonly string[]): Promise<void>;
|
|
14
14
|
export { main };
|
package/dist/cli/create.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Notes:
|
|
11
11
|
* - Template can be a short name (resolved via DOMINDS_TEMPLATE_BASE) or a git URL.
|
|
12
|
-
* - rtws directory is `process.cwd()`. Use 'dominds -C <dir> create ...' to create under another base dir.
|
|
12
|
+
* - rtws directory is `process.cwd()`. Use 'dominds -C <abs-dir> create ...' to create under another base dir.
|
|
13
13
|
*/
|
|
14
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
15
|
if (k2 === undefined) k2 = k;
|
package/dist/cli/read.js
CHANGED
|
@@ -59,7 +59,7 @@ function printUsage() {
|
|
|
59
59
|
console.log('`--audit` runs prompt audit via hidden teammate @fuxi using default LLM config (skips when unavailable), and also includes static toolset checks (registry vs `.minds/mcp.yaml` declarations).');
|
|
60
60
|
console.log('When <member-id> is omitted, reads all visible team members.');
|
|
61
61
|
console.log('');
|
|
62
|
-
console.log("Note: rtws (runtime workspace) directory is `process.cwd()`. Use 'dominds -C <dir> read' to run in another rtws.");
|
|
62
|
+
console.log("Note: rtws (runtime workspace) directory is `process.cwd()`. Use 'dominds -C <abs-dir> read' to run in another rtws.");
|
|
63
63
|
console.log('');
|
|
64
64
|
console.log('Examples:');
|
|
65
65
|
console.log(' dominds read # Read all team members');
|
package/dist/cli/tui.js
CHANGED
|
@@ -76,7 +76,7 @@ function showHelp() {
|
|
|
76
76
|
console.log('');
|
|
77
77
|
console.log('Start or continue a dialog with an AI team member using a Taskdoc.');
|
|
78
78
|
console.log('');
|
|
79
|
-
console.log("Note: rtws (runtime workspace) directory is `process.cwd()`. Use 'dominds -C <dir> tui ...' to run in another rtws.");
|
|
79
|
+
console.log("Note: rtws (runtime workspace) directory is `process.cwd()`. Use 'dominds -C <abs-dir> tui ...' to run in another rtws.");
|
|
80
80
|
console.log('');
|
|
81
81
|
console.log('Arguments:');
|
|
82
82
|
console.log(' <taskdoc-path> Path to Taskdoc (required for dialog)');
|
package/dist/cli/webui.js
CHANGED
|
@@ -30,7 +30,7 @@ Usage:
|
|
|
30
30
|
dominds webui [options]
|
|
31
31
|
|
|
32
32
|
Note:
|
|
33
|
-
rtws (runtime workspace) directory is \`process.cwd()\`. Use 'dominds -C <dir> webui' to run in another rtws.
|
|
33
|
+
rtws (runtime workspace) directory is \`process.cwd()\`. Use 'dominds -C <abs-dir> webui' to run in another rtws.
|
|
34
34
|
|
|
35
35
|
Options:
|
|
36
36
|
-p, --port <port> Port to listen on. Bare port is strict; suffix + tries higher ports; suffix - tries lower ports.
|
package/dist/cli.js
CHANGED
|
@@ -107,10 +107,10 @@ function printHelp() {
|
|
|
107
107
|
Dominds CLI - AI-driven DevOps framework with persistent memory
|
|
108
108
|
|
|
109
109
|
Usage:
|
|
110
|
-
dominds [-C <dir>] [subcommand] [options]
|
|
110
|
+
dominds [-C <abs-dir>] [subcommand] [options]
|
|
111
111
|
|
|
112
112
|
Global Options:
|
|
113
|
-
-C <dir>
|
|
113
|
+
-C <abs-dir> Change to absolute runtime workspace directory (rtws) before running
|
|
114
114
|
|
|
115
115
|
Subcommands:
|
|
116
116
|
webui [options] Start WebUI server (default)
|
|
@@ -133,7 +133,7 @@ Subcommands:
|
|
|
133
133
|
Examples:
|
|
134
134
|
dominds # Start WebUI server (default)
|
|
135
135
|
dominds webui # Start WebUI server
|
|
136
|
-
dominds -C
|
|
136
|
+
dominds -C /path/to/my-ws webui # Start in specific rtws
|
|
137
137
|
dominds tui --help # Show TUI help
|
|
138
138
|
dominds run task.tsk # Run task dialog
|
|
139
139
|
dominds read # Read team configuration
|
|
@@ -159,10 +159,9 @@ function printVersion() {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
async function main(argv = process.argv.slice(2)) {
|
|
162
|
-
const baseCwd = process.cwd();
|
|
163
162
|
let parsed;
|
|
164
163
|
try {
|
|
165
|
-
parsed = (0, rtws_cli_1.extractGlobalRtwsChdir)({ argv
|
|
164
|
+
parsed = (0, rtws_cli_1.extractGlobalRtwsChdir)({ argv });
|
|
166
165
|
}
|
|
167
166
|
catch (err) {
|
|
168
167
|
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
@@ -173,10 +172,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
173
172
|
if (args.length === 0) {
|
|
174
173
|
if (parsed.chdir) {
|
|
175
174
|
try {
|
|
176
|
-
|
|
177
|
-
? parsed.chdir
|
|
178
|
-
: path.resolve(baseCwd, parsed.chdir);
|
|
179
|
-
process.chdir(absoluteRtwsRoot);
|
|
175
|
+
process.chdir(parsed.chdir);
|
|
180
176
|
}
|
|
181
177
|
catch (err) {
|
|
182
178
|
console.error(`Error: failed to change directory to '${parsed.chdir}':`, err);
|
|
@@ -222,10 +218,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
222
218
|
if (!shouldSkipRtwsSetup) {
|
|
223
219
|
if (parsed.chdir) {
|
|
224
220
|
try {
|
|
225
|
-
|
|
226
|
-
? parsed.chdir
|
|
227
|
-
: path.resolve(baseCwd, parsed.chdir);
|
|
228
|
-
process.chdir(absoluteRtwsRoot);
|
|
221
|
+
process.chdir(parsed.chdir);
|
|
229
222
|
}
|
|
230
223
|
catch (err) {
|
|
231
224
|
console.error(`Error: failed to change directory to '${parsed.chdir}':`, err);
|
package/dist/docs/cli-usage.md
CHANGED
|
@@ -4,7 +4,7 @@ Chinese version: [中文版](./cli-usage.zh.md)
|
|
|
4
4
|
|
|
5
5
|
The `dominds` CLI provides a unified entry point, but the **primary interaction experience is the Web UI** (the default `dominds` command). This guide focuses on the Web UI workflow.
|
|
6
6
|
|
|
7
|
-
> Note: In this document, **rtws (runtime workspace)** refers to the runtime root directory Dominds uses (by default `process.cwd()`, switchable via `-C <dir
|
|
7
|
+
> Note: In this document, **rtws (runtime workspace)** refers to the runtime root directory Dominds uses (by default `process.cwd()`, switchable via `-C <abs-dir>`; `-C` only accepts absolute paths).
|
|
8
8
|
|
|
9
9
|
> Note: `dominds tui` / `dominds run` are currently reserved subcommand names and do not have a stable implementation yet. As a result, this guide does not document TUI options or detailed usage.
|
|
10
10
|
|
|
@@ -49,7 +49,7 @@ dominds webui [options]
|
|
|
49
49
|
|
|
50
50
|
# Common: choose port / rtws
|
|
51
51
|
dominds webui -p 8080
|
|
52
|
-
dominds webui -C
|
|
52
|
+
dominds webui -C /path/to/my-rtws
|
|
53
53
|
|
|
54
54
|
# Minds reader: inspect team configuration
|
|
55
55
|
dominds read [options] [member-id]
|
|
@@ -83,7 +83,7 @@ Start the web-based user interface for the current rtws. This provides a graphic
|
|
|
83
83
|
|
|
84
84
|
- `-p, --port <port>` - Port to listen on; a bare port binds strictly, suffix `+` tries higher ports, and suffix `-` tries lower ports (omitting `--port` is equivalent to `5666-`)
|
|
85
85
|
- `-h, --host <host>` - Host to bind to (default: localhost)
|
|
86
|
-
- `-C, --cwd <dir>` - Change to rtws directory before starting
|
|
86
|
+
- `-C, --cwd <abs-dir>` - Change to rtws directory before starting; absolute paths only
|
|
87
87
|
- `--help` - Show help message
|
|
88
88
|
|
|
89
89
|
**Examples:**
|
|
@@ -92,7 +92,7 @@ Start the web-based user interface for the current rtws. This provides a graphic
|
|
|
92
92
|
dominds
|
|
93
93
|
dominds webui -p 8080
|
|
94
94
|
dominds webui -p 8080+
|
|
95
|
-
dominds webui -C
|
|
95
|
+
dominds webui -C /path/to/my-rtws
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
**Common use cases:**
|
|
@@ -122,7 +122,7 @@ Read and inspect agent prompts/configuration for the rtws. This is commonly used
|
|
|
122
122
|
|
|
123
123
|
**Options:**
|
|
124
124
|
|
|
125
|
-
- `-C, --cwd <dir>` - Change to rtws directory before reading
|
|
125
|
+
- `-C, --cwd <abs-dir>` - Change to rtws directory before reading; absolute paths only
|
|
126
126
|
- `--only-prompt` - Show only system prompts
|
|
127
127
|
- `--only-mem` - Show only memory
|
|
128
128
|
- `--help` - Show help message
|
|
@@ -132,7 +132,7 @@ Read and inspect agent prompts/configuration for the rtws. This is commonly used
|
|
|
132
132
|
```bash
|
|
133
133
|
dominds read
|
|
134
134
|
dominds read developer
|
|
135
|
-
dominds read -C
|
|
135
|
+
dominds read -C /path/to/my-rtws
|
|
136
136
|
dominds read --only-prompt
|
|
137
137
|
dominds read --only-mem
|
|
138
138
|
```
|
|
@@ -190,7 +190,7 @@ dominds
|
|
|
190
190
|
dominds webui -p 8080
|
|
191
191
|
|
|
192
192
|
# 3) Start in a specific rtws
|
|
193
|
-
dominds webui -C
|
|
193
|
+
dominds webui -C /path/to/my-rtws
|
|
194
194
|
|
|
195
195
|
# 4) Inspect current team/rtws configuration
|
|
196
196
|
dominds read
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`dominds` 提供统一的命令行入口,但**主要交互界面是 Web UI**(默认命令 `dominds`)。本文档以 Web UI 工作流为主。
|
|
6
6
|
|
|
7
|
-
> 注:本文统一使用 **rtws(运行时工作区)** 表示 Dominds 运行时使用的根目录(默认等于 `process.cwd()`,可通过 `-C <dir>`
|
|
7
|
+
> 注:本文统一使用 **rtws(运行时工作区)** 表示 Dominds 运行时使用的根目录(默认等于 `process.cwd()`,可通过 `-C <abs-dir>` 切换;`-C` 只接受绝对路径)。
|
|
8
8
|
|
|
9
9
|
> 说明:`dominds tui` / `dominds run` 相关功能目前尚未提供稳定实现(子命令名保留用于未来规划),因此本指南不再展开 TUI 的命令选项与用法细节。
|
|
10
10
|
|
|
@@ -49,7 +49,7 @@ dominds webui [options]
|
|
|
49
49
|
|
|
50
50
|
# 常用:指定端口 / rtws
|
|
51
51
|
dominds webui -p 8080
|
|
52
|
-
dominds webui -C
|
|
52
|
+
dominds webui -C /path/to/my-rtws
|
|
53
53
|
|
|
54
54
|
# Minds 阅读器:分析团队配置
|
|
55
55
|
dominds read [options] [member-id]
|
|
@@ -83,7 +83,7 @@ dominds webui [options]
|
|
|
83
83
|
|
|
84
84
|
- `-p, --port <port>` - 监听端口;裸端口严格绑定,后缀 `+` 向更大端口自动尝试,后缀 `-` 向更小端口自动尝试(未指定时等价于 `5666-`)
|
|
85
85
|
- `-h, --host <host>` - 绑定的主机(默认:localhost)
|
|
86
|
-
- `-C, --cwd <dir>` - 启动前更改 rtws
|
|
86
|
+
- `-C, --cwd <abs-dir>` - 启动前更改 rtws 目录;只接受绝对路径
|
|
87
87
|
- `--help` - 显示帮助消息
|
|
88
88
|
|
|
89
89
|
**示例:**
|
|
@@ -99,7 +99,7 @@ dominds webui -p 8080
|
|
|
99
99
|
dominds webui -p 8080+
|
|
100
100
|
|
|
101
101
|
# 在特定 rtws 启动 Web UI
|
|
102
|
-
dominds webui -C
|
|
102
|
+
dominds webui -C /path/to/my-rtws
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
**常见用途:**
|
|
@@ -129,7 +129,7 @@ dominds read [options] [member-id]
|
|
|
129
129
|
|
|
130
130
|
**选项:**
|
|
131
131
|
|
|
132
|
-
- `-C, --cwd <dir>` - 读取前更改 rtws
|
|
132
|
+
- `-C, --cwd <abs-dir>` - 读取前更改 rtws 目录;只接受绝对路径
|
|
133
133
|
- `--only-prompt` - 仅显示系统提示词
|
|
134
134
|
- `--only-mem` - 仅显示内存
|
|
135
135
|
- `--help` - 显示帮助消息
|
|
@@ -139,7 +139,7 @@ dominds read [options] [member-id]
|
|
|
139
139
|
```bash
|
|
140
140
|
dominds read
|
|
141
141
|
dominds read developer
|
|
142
|
-
dominds read -C
|
|
142
|
+
dominds read -C /path/to/my-rtws
|
|
143
143
|
dominds read --only-prompt
|
|
144
144
|
dominds read --only-mem
|
|
145
145
|
```
|
|
@@ -204,7 +204,7 @@ dominds
|
|
|
204
204
|
dominds webui -p 8080
|
|
205
205
|
|
|
206
206
|
# 3) 在特定 rtws 启动
|
|
207
|
-
dominds webui -C
|
|
207
|
+
dominds webui -C /path/to/my-rtws
|
|
208
208
|
|
|
209
209
|
# 4) 查看/核对当前团队配置
|
|
210
210
|
dominds read
|
|
@@ -4,7 +4,7 @@ Chinese version: [中文版](./dialog-persistence.zh.md)
|
|
|
4
4
|
|
|
5
5
|
This document describes the persistence layer and storage mechanisms for the Dominds dialog system, including file system conventions, data structures, and storage patterns.
|
|
6
6
|
|
|
7
|
-
> Note: In this document, **rtws (runtime workspace)** refers to Dominds' runtime root directory (by default `process.cwd()`, switchable via `-C <dir
|
|
7
|
+
> Note: In this document, **rtws (runtime workspace)** refers to Dominds' runtime root directory (by default `process.cwd()`, switchable via `-C <abs-dir>`; `-C` only accepts absolute paths).
|
|
8
8
|
|
|
9
9
|
## Current Implementation Status
|
|
10
10
|
|
|
@@ -264,8 +264,8 @@ Example / 示例(概念):
|
|
|
264
264
|
|
|
265
265
|
### rtws(运行时工作区)
|
|
266
266
|
|
|
267
|
-
- EN: **rtws** (runtime workspace) is the directory Dominds treats as its runtime root (by default it is `process.cwd()`, and can be changed via `-C <dir
|
|
268
|
-
- ZH: **rtws(运行时工作区)**是 Dominds 运行时使用的根目录(默认等于 `process.cwd()`,可通过 `-C <dir>`
|
|
267
|
+
- EN: **rtws** (runtime workspace) is the directory Dominds treats as its runtime root (by default it is `process.cwd()`, and can be changed via `-C <abs-dir>`; `-C` only accepts absolute paths). Files like `.minds/` and `.dialogs/` live under the rtws.
|
|
268
|
+
- ZH: **rtws(运行时工作区)**是 Dominds 运行时使用的根目录(默认等于 `process.cwd()`,可通过 `-C <abs-dir>` 切换;`-C` 只接受绝对路径)。诸如 `.minds/`、`.dialogs/` 等运行态目录均位于 rtws 下。
|
|
269
269
|
|
|
270
270
|
- EN: Wording rule: when the meaning is **rtws**, prefer writing “rtws (runtime workspace)” (or just “rtws” after the first mention) rather than the ambiguous generic “workspace”.
|
|
271
271
|
- ZH: 用词规则:当语义指向 **rtws** 时,优先写“rtws(运行时工作区)”(或在已定义后只写“rtws”),避免在对外提示/文档中只写“工作区”从而与其他语境的 workspace/workdir 混淆。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type RestartHelperStdioMode = 'inherit' | 'ignore';
|
|
2
|
+
export type RestartHelperPayload = Readonly<{
|
|
3
|
+
command: string;
|
|
4
|
+
args: readonly string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
retiringPid: number;
|
|
9
|
+
forceKillAfterMs: number;
|
|
10
|
+
probeIntervalMs: number;
|
|
11
|
+
portReleaseTimeoutMs: number;
|
|
12
|
+
stdioMode: RestartHelperStdioMode;
|
|
13
|
+
traceFile: string;
|
|
14
|
+
debugDir: string;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const net_1 = __importDefault(require("net"));
|
|
9
|
+
const time_1 = require("@longrun-ai/kernel/utils/time");
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function isStringArray(value) {
|
|
14
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
15
|
+
}
|
|
16
|
+
function readString(value, key) {
|
|
17
|
+
const candidate = value[key];
|
|
18
|
+
if (typeof candidate !== 'string' || candidate.trim() === '') {
|
|
19
|
+
throw new Error(`Invalid Dominds restart helper payload: ${key} must be a non-empty string`);
|
|
20
|
+
}
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
function readNumber(value, key) {
|
|
24
|
+
const candidate = value[key];
|
|
25
|
+
if (typeof candidate !== 'number' || !Number.isFinite(candidate)) {
|
|
26
|
+
throw new Error(`Invalid Dominds restart helper payload: ${key} must be a finite number`);
|
|
27
|
+
}
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
function readPositiveInteger(value, key) {
|
|
31
|
+
const candidate = readNumber(value, key);
|
|
32
|
+
if (!Number.isInteger(candidate) || candidate <= 0) {
|
|
33
|
+
throw new Error(`Invalid Dominds restart helper payload: ${key} must be a positive integer`);
|
|
34
|
+
}
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
function readPort(value, key) {
|
|
38
|
+
const candidate = readPositiveInteger(value, key);
|
|
39
|
+
if (candidate > 65535) {
|
|
40
|
+
throw new Error(`Invalid Dominds restart helper payload: ${key} must be <= 65535`);
|
|
41
|
+
}
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
function parsePayload(raw) {
|
|
45
|
+
if (typeof raw !== 'string' || raw.trim() === '') {
|
|
46
|
+
throw new Error('Missing Dominds restart helper payload');
|
|
47
|
+
}
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
if (!isRecord(parsed)) {
|
|
50
|
+
throw new Error('Invalid Dominds restart helper payload: expected an object');
|
|
51
|
+
}
|
|
52
|
+
const args = parsed['args'];
|
|
53
|
+
if (!isStringArray(args)) {
|
|
54
|
+
throw new Error('Invalid Dominds restart helper payload: args must be a string array');
|
|
55
|
+
}
|
|
56
|
+
const stdioMode = parsed['stdioMode'];
|
|
57
|
+
if (stdioMode !== 'inherit' && stdioMode !== 'ignore') {
|
|
58
|
+
throw new Error('Invalid Dominds restart helper payload: stdioMode must be inherit or ignore');
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
command: readString(parsed, 'command'),
|
|
62
|
+
args,
|
|
63
|
+
cwd: readString(parsed, 'cwd'),
|
|
64
|
+
host: readString(parsed, 'host'),
|
|
65
|
+
port: readPort(parsed, 'port'),
|
|
66
|
+
retiringPid: readPositiveInteger(parsed, 'retiringPid'),
|
|
67
|
+
forceKillAfterMs: readPositiveInteger(parsed, 'forceKillAfterMs'),
|
|
68
|
+
probeIntervalMs: readPositiveInteger(parsed, 'probeIntervalMs'),
|
|
69
|
+
portReleaseTimeoutMs: readPositiveInteger(parsed, 'portReleaseTimeoutMs'),
|
|
70
|
+
stdioMode,
|
|
71
|
+
traceFile: readString(parsed, 'traceFile'),
|
|
72
|
+
debugDir: readString(parsed, 'debugDir'),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function trace(payload, event, details = {}) {
|
|
76
|
+
const record = {
|
|
77
|
+
...details,
|
|
78
|
+
event,
|
|
79
|
+
capturedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
|
|
80
|
+
helperPid: process.pid,
|
|
81
|
+
platform: process.platform,
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
fs_1.default.mkdirSync(payload.debugDir, { recursive: true });
|
|
85
|
+
fs_1.default.appendFileSync(payload.traceFile, `${JSON.stringify(record)}\n`, 'utf8');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
console.error(`Failed to write Dominds restart helper trace: ${message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function isPortBusy(payload) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const socket = net_1.default.createConnection({ host: payload.host, port: payload.port });
|
|
95
|
+
let settled = false;
|
|
96
|
+
const finish = (busy) => {
|
|
97
|
+
if (settled)
|
|
98
|
+
return;
|
|
99
|
+
settled = true;
|
|
100
|
+
socket.destroy();
|
|
101
|
+
resolve(busy);
|
|
102
|
+
};
|
|
103
|
+
socket.once('connect', () => finish(true));
|
|
104
|
+
socket.once('error', () => finish(false));
|
|
105
|
+
socket.setTimeout(1000, () => finish(true));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function getErrorCode(error) {
|
|
109
|
+
return isRecord(error) && typeof error['code'] === 'string' ? error['code'] : '';
|
|
110
|
+
}
|
|
111
|
+
function assertValidRetiringPid(payload) {
|
|
112
|
+
if (!Number.isInteger(payload.retiringPid) || payload.retiringPid <= 0) {
|
|
113
|
+
throw new Error(`Invalid retiring Dominds pid for restart: ${String(payload.retiringPid)}`);
|
|
114
|
+
}
|
|
115
|
+
if (payload.retiringPid === process.pid) {
|
|
116
|
+
throw new Error(`Refusing to kill restart helper pid ${String(process.pid)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isRetiringProcessAlive(payload) {
|
|
120
|
+
assertValidRetiringPid(payload);
|
|
121
|
+
try {
|
|
122
|
+
process.kill(payload.retiringPid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const code = getErrorCode(error);
|
|
127
|
+
if (code === 'ESRCH')
|
|
128
|
+
return false;
|
|
129
|
+
if (code === 'EPERM')
|
|
130
|
+
return true;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function delayMs(ms) {
|
|
135
|
+
await new Promise((resolve) => {
|
|
136
|
+
setTimeout(resolve, ms);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async function waitForRetiringProcessExit(payload, timeoutMs) {
|
|
140
|
+
const deadline = Date.now() + timeoutMs;
|
|
141
|
+
while (Date.now() < deadline) {
|
|
142
|
+
if (!isRetiringProcessAlive(payload))
|
|
143
|
+
return true;
|
|
144
|
+
await delayMs(payload.probeIntervalMs);
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
async function waitForPortReleaseUntil(payload, deadline) {
|
|
149
|
+
let consecutiveReady = 0;
|
|
150
|
+
while (Date.now() < deadline) {
|
|
151
|
+
if (!(await isPortBusy(payload))) {
|
|
152
|
+
consecutiveReady += 1;
|
|
153
|
+
if (consecutiveReady >= 2)
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
consecutiveReady = 0;
|
|
158
|
+
}
|
|
159
|
+
await delayMs(payload.probeIntervalMs);
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
async function runBestEffortKiller(payload, command, args) {
|
|
164
|
+
await new Promise((resolve) => {
|
|
165
|
+
trace(payload, 'helper.force_kill.spawn', { command, args });
|
|
166
|
+
const killer = (0, child_process_1.spawn)(command, [...args], {
|
|
167
|
+
stdio: payload.stdioMode,
|
|
168
|
+
windowsHide: payload.stdioMode !== 'inherit',
|
|
169
|
+
});
|
|
170
|
+
killer.once('error', (error) => {
|
|
171
|
+
trace(payload, 'helper.force_kill.error', { message: error.message });
|
|
172
|
+
resolve();
|
|
173
|
+
});
|
|
174
|
+
killer.once('exit', (code, signal) => {
|
|
175
|
+
trace(payload, 'helper.force_kill.exit', { code, signal });
|
|
176
|
+
resolve();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async function forceKillRetiringProcess(payload) {
|
|
181
|
+
assertValidRetiringPid(payload);
|
|
182
|
+
try {
|
|
183
|
+
process.kill(payload.retiringPid, 'SIGKILL');
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const code = getErrorCode(error);
|
|
187
|
+
if (code === 'ESRCH')
|
|
188
|
+
return;
|
|
189
|
+
if (process.platform !== 'win32')
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
if (process.platform === 'win32') {
|
|
193
|
+
await runBestEffortKiller(payload, 'taskkill.exe', ['/PID', String(payload.retiringPid), '/F']);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function runRestartHelper(payload) {
|
|
197
|
+
const detached = payload.stdioMode !== 'inherit';
|
|
198
|
+
trace(payload, 'helper.start', {
|
|
199
|
+
command: payload.command,
|
|
200
|
+
args: payload.args,
|
|
201
|
+
cwd: payload.cwd,
|
|
202
|
+
host: payload.host,
|
|
203
|
+
port: payload.port,
|
|
204
|
+
retiringPid: payload.retiringPid,
|
|
205
|
+
detached,
|
|
206
|
+
stdioMode: payload.stdioMode,
|
|
207
|
+
forceKillAfterMs: payload.forceKillAfterMs,
|
|
208
|
+
portReleaseTimeoutMs: payload.portReleaseTimeoutMs,
|
|
209
|
+
probeIntervalMs: payload.probeIntervalMs,
|
|
210
|
+
});
|
|
211
|
+
const forceKillDeadline = Date.now() + payload.forceKillAfterMs;
|
|
212
|
+
trace(payload, 'helper.wait_retiring_process_exit.start', {
|
|
213
|
+
retiringPid: payload.retiringPid,
|
|
214
|
+
timeoutMs: payload.forceKillAfterMs,
|
|
215
|
+
});
|
|
216
|
+
const exitedGracefully = await waitForRetiringProcessExit(payload, payload.forceKillAfterMs);
|
|
217
|
+
trace(payload, 'helper.wait_retiring_process_exit.finish', {
|
|
218
|
+
retiringPid: payload.retiringPid,
|
|
219
|
+
exited: exitedGracefully,
|
|
220
|
+
});
|
|
221
|
+
if (!exitedGracefully) {
|
|
222
|
+
trace(payload, 'helper.force_kill.start', { retiringPid: payload.retiringPid });
|
|
223
|
+
await forceKillRetiringProcess(payload);
|
|
224
|
+
trace(payload, 'helper.force_kill.finish', { retiringPid: payload.retiringPid });
|
|
225
|
+
trace(payload, 'helper.wait_retiring_process_exit_after_kill.start', {
|
|
226
|
+
retiringPid: payload.retiringPid,
|
|
227
|
+
timeoutMs: payload.portReleaseTimeoutMs,
|
|
228
|
+
});
|
|
229
|
+
const exitedAfterKill = await waitForRetiringProcessExit(payload, payload.portReleaseTimeoutMs);
|
|
230
|
+
trace(payload, 'helper.wait_retiring_process_exit_after_kill.finish', {
|
|
231
|
+
retiringPid: payload.retiringPid,
|
|
232
|
+
exited: exitedAfterKill,
|
|
233
|
+
});
|
|
234
|
+
if (!exitedAfterKill) {
|
|
235
|
+
throw new Error(`Dominds retiring process is still alive after force-killing pid ${String(payload.retiringPid)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const portReleaseDeadline = Math.max(forceKillDeadline, Date.now() + payload.portReleaseTimeoutMs);
|
|
239
|
+
trace(payload, 'helper.wait_port_release.start', {
|
|
240
|
+
host: payload.host,
|
|
241
|
+
port: payload.port,
|
|
242
|
+
deadlineMsFromNow: portReleaseDeadline - Date.now(),
|
|
243
|
+
});
|
|
244
|
+
const portReleased = await waitForPortReleaseUntil(payload, portReleaseDeadline);
|
|
245
|
+
trace(payload, 'helper.wait_port_release.finish', {
|
|
246
|
+
host: payload.host,
|
|
247
|
+
port: payload.port,
|
|
248
|
+
released: portReleased,
|
|
249
|
+
});
|
|
250
|
+
if (!portReleased) {
|
|
251
|
+
throw new Error(`Dominds restart port is still busy after retiring pid ${String(payload.retiringPid)} exited; port=${String(payload.host)}:${String(payload.port)}`);
|
|
252
|
+
}
|
|
253
|
+
trace(payload, 'helper.spawn_new_process.start', {
|
|
254
|
+
command: payload.command,
|
|
255
|
+
args: payload.args,
|
|
256
|
+
cwd: payload.cwd,
|
|
257
|
+
detached,
|
|
258
|
+
stdioMode: payload.stdioMode,
|
|
259
|
+
});
|
|
260
|
+
const child = (0, child_process_1.spawn)(payload.command, [...payload.args], {
|
|
261
|
+
cwd: payload.cwd,
|
|
262
|
+
env: process.env,
|
|
263
|
+
detached,
|
|
264
|
+
stdio: payload.stdioMode,
|
|
265
|
+
shell: false,
|
|
266
|
+
windowsHide: payload.stdioMode !== 'inherit',
|
|
267
|
+
});
|
|
268
|
+
if (detached)
|
|
269
|
+
child.unref();
|
|
270
|
+
await new Promise((resolve, reject) => {
|
|
271
|
+
child.once('error', (error) => {
|
|
272
|
+
trace(payload, 'helper.spawn_new_process.error', {
|
|
273
|
+
command: payload.command,
|
|
274
|
+
args: payload.args,
|
|
275
|
+
cwd: payload.cwd,
|
|
276
|
+
message: error.message,
|
|
277
|
+
stack: error.stack ?? null,
|
|
278
|
+
});
|
|
279
|
+
reject(error);
|
|
280
|
+
});
|
|
281
|
+
child.once('spawn', resolve);
|
|
282
|
+
});
|
|
283
|
+
trace(payload, 'helper.spawn_new_process.finish', { childPid: child.pid ?? null });
|
|
284
|
+
trace(payload, 'helper.exit', { code: 0 });
|
|
285
|
+
}
|
|
286
|
+
async function main() {
|
|
287
|
+
let payload = null;
|
|
288
|
+
try {
|
|
289
|
+
payload = parsePayload(process.argv[2]);
|
|
290
|
+
await runRestartHelper(payload);
|
|
291
|
+
process.exit(0);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
295
|
+
const stack = error instanceof Error ? (error.stack ?? null) : null;
|
|
296
|
+
if (payload !== null) {
|
|
297
|
+
trace(payload, 'helper.error', { message, stack });
|
|
298
|
+
}
|
|
299
|
+
console.error(message);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (require.main === module) {
|
|
304
|
+
void main();
|
|
305
|
+
}
|
|
@@ -10,6 +10,7 @@ exports.getDomindsSelfUpdateStatus = getDomindsSelfUpdateStatus;
|
|
|
10
10
|
exports.installLatestDominds = installLatestDominds;
|
|
11
11
|
exports.restartDomindsIntoLatest = restartDomindsIntoLatest;
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
13
14
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
14
15
|
const path_1 = __importDefault(require("path"));
|
|
15
16
|
const time_1 = require("@longrun-ai/kernel/utils/time");
|
|
@@ -43,6 +44,7 @@ let restartPromise = null;
|
|
|
43
44
|
let restartState = IDLE_RESTART_STATE;
|
|
44
45
|
let installFailureObservation = null;
|
|
45
46
|
let broadcastStatusUpdate = null;
|
|
47
|
+
let activeRestartTraceFile = null;
|
|
46
48
|
function normalizeVersionString(value) {
|
|
47
49
|
return value.trim().replace(/^v/i, '');
|
|
48
50
|
}
|
|
@@ -101,6 +103,55 @@ function normalizeComparablePath(value) {
|
|
|
101
103
|
const normalized = path_1.default.normalize(value);
|
|
102
104
|
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
103
105
|
}
|
|
106
|
+
function sanitizeDebugFileSegment(value) {
|
|
107
|
+
const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
108
|
+
return sanitized.length > 0 ? sanitized.slice(0, 80) : 'unknown';
|
|
109
|
+
}
|
|
110
|
+
function buildRestartTraceContext() {
|
|
111
|
+
const capturedAt = (0, time_1.formatUnifiedTimestamp)(new Date());
|
|
112
|
+
const debugDir = path_1.default.resolve(process.cwd(), '.dialogs', 'debug');
|
|
113
|
+
const traceFile = path_1.default.join(debugDir, [
|
|
114
|
+
'dominds-self-update-restart',
|
|
115
|
+
sanitizeDebugFileSegment(capturedAt),
|
|
116
|
+
String(process.pid),
|
|
117
|
+
`${(0, crypto_1.randomUUID)()}.jsonl`,
|
|
118
|
+
].join('-'));
|
|
119
|
+
return { debugDir, traceFile };
|
|
120
|
+
}
|
|
121
|
+
function getRestartHelperEntrypoint() {
|
|
122
|
+
return path_1.default.resolve(__dirname, 'dominds-self-update-restart-helper.js');
|
|
123
|
+
}
|
|
124
|
+
async function appendRestartTrace(trace, event, details = {}) {
|
|
125
|
+
const payload = {
|
|
126
|
+
...details,
|
|
127
|
+
event,
|
|
128
|
+
capturedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
|
|
129
|
+
pid: process.pid,
|
|
130
|
+
platform: process.platform,
|
|
131
|
+
rtwsRootAbs: process.cwd(),
|
|
132
|
+
};
|
|
133
|
+
await promises_1.default.mkdir(trace.debugDir, { recursive: true });
|
|
134
|
+
await promises_1.default.appendFile(trace.traceFile, `${JSON.stringify(payload)}\n`, 'utf-8');
|
|
135
|
+
}
|
|
136
|
+
function appendRestartTraceSoon(trace, event, details = {}) {
|
|
137
|
+
void appendRestartTrace(trace, event, details).catch((error) => {
|
|
138
|
+
log.warn('Failed to write Dominds restart trace', error, {
|
|
139
|
+
traceFile: trace.traceFile,
|
|
140
|
+
event,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function appendRestartTraceBestEffort(trace, event, details = {}) {
|
|
145
|
+
try {
|
|
146
|
+
await appendRestartTrace(trace, event, details);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
log.warn('Failed to write Dominds restart trace', error, {
|
|
150
|
+
traceFile: trace.traceFile,
|
|
151
|
+
event,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
104
155
|
async function getComparableRealPath(absPath) {
|
|
105
156
|
try {
|
|
106
157
|
return normalizeComparablePath(await promises_1.default.realpath(absPath));
|
|
@@ -1331,9 +1382,11 @@ async function installLatestDominds() {
|
|
|
1331
1382
|
publishStatusUpdateSoon();
|
|
1332
1383
|
return await getDomindsSelfUpdateStatus();
|
|
1333
1384
|
}
|
|
1334
|
-
function spawnDetachedRestartHelper(params) {
|
|
1385
|
+
async function spawnDetachedRestartHelper(params) {
|
|
1335
1386
|
const stdioMode = getRestartHelperStdio();
|
|
1336
|
-
const
|
|
1387
|
+
const helperEntrypoint = getRestartHelperEntrypoint();
|
|
1388
|
+
await promises_1.default.access(helperEntrypoint);
|
|
1389
|
+
const helperPayload = {
|
|
1337
1390
|
command: params.command,
|
|
1338
1391
|
args: [...params.args],
|
|
1339
1392
|
cwd: params.cwd,
|
|
@@ -1344,145 +1397,88 @@ function spawnDetachedRestartHelper(params) {
|
|
|
1344
1397
|
probeIntervalMs: RESTART_PORT_PROBE_INTERVAL_MS,
|
|
1345
1398
|
portReleaseTimeoutMs: RESTART_PORT_RELEASE_TIMEOUT_MS,
|
|
1346
1399
|
stdioMode,
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
'const payload = JSON.parse(process.argv[1]);',
|
|
1352
|
-
'const detached = payload.stdioMode !== "inherit";',
|
|
1353
|
-
'function isPortBusy() {',
|
|
1354
|
-
' return new Promise((resolve) => {',
|
|
1355
|
-
' const socket = net.createConnection({ host: payload.host, port: payload.port });',
|
|
1356
|
-
' let settled = false;',
|
|
1357
|
-
' const finish = (busy) => {',
|
|
1358
|
-
' if (settled) return;',
|
|
1359
|
-
' settled = true;',
|
|
1360
|
-
' socket.destroy();',
|
|
1361
|
-
' resolve(busy);',
|
|
1362
|
-
' };',
|
|
1363
|
-
' socket.once("connect", () => finish(true));',
|
|
1364
|
-
' socket.once("error", () => finish(false));',
|
|
1365
|
-
' socket.setTimeout(1000, () => finish(true));',
|
|
1366
|
-
' });',
|
|
1367
|
-
'}',
|
|
1368
|
-
'function getErrorCode(error) {',
|
|
1369
|
-
' return error && typeof error === "object" && typeof error.code === "string" ? error.code : "";',
|
|
1370
|
-
'}',
|
|
1371
|
-
'function assertValidRetiringPid() {',
|
|
1372
|
-
' if (!Number.isInteger(payload.retiringPid) || payload.retiringPid <= 0) {',
|
|
1373
|
-
' throw new Error(`Invalid retiring Dominds pid for restart: ${String(payload.retiringPid)}`);',
|
|
1374
|
-
' }',
|
|
1375
|
-
' if (payload.retiringPid === process.pid) {',
|
|
1376
|
-
' throw new Error(`Refusing to kill restart helper pid ${String(process.pid)}`);',
|
|
1377
|
-
' }',
|
|
1378
|
-
'}',
|
|
1379
|
-
'function isRetiringProcessAlive() {',
|
|
1380
|
-
' assertValidRetiringPid();',
|
|
1381
|
-
' try {',
|
|
1382
|
-
' process.kill(payload.retiringPid, 0);',
|
|
1383
|
-
' return true;',
|
|
1384
|
-
' } catch (error) {',
|
|
1385
|
-
' const code = getErrorCode(error);',
|
|
1386
|
-
' if (code === "ESRCH") return false;',
|
|
1387
|
-
' if (code === "EPERM") return true;',
|
|
1388
|
-
' throw error;',
|
|
1389
|
-
' }',
|
|
1390
|
-
'}',
|
|
1391
|
-
'async function waitForRetiringProcessExit(timeoutMs) {',
|
|
1392
|
-
' const deadline = Date.now() + timeoutMs;',
|
|
1393
|
-
' while (Date.now() < deadline) {',
|
|
1394
|
-
' if (!isRetiringProcessAlive()) return true;',
|
|
1395
|
-
' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
|
|
1396
|
-
' }',
|
|
1397
|
-
' return false;',
|
|
1398
|
-
'}',
|
|
1399
|
-
'async function waitForPortReleaseUntil(deadline) {',
|
|
1400
|
-
' let consecutiveReady = 0;',
|
|
1401
|
-
' while (Date.now() < deadline) {',
|
|
1402
|
-
' if (!(await isPortBusy())) {',
|
|
1403
|
-
' consecutiveReady += 1;',
|
|
1404
|
-
' if (consecutiveReady >= 2) return true;',
|
|
1405
|
-
' } else {',
|
|
1406
|
-
' consecutiveReady = 0;',
|
|
1407
|
-
' }',
|
|
1408
|
-
' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
|
|
1409
|
-
' }',
|
|
1410
|
-
' return false;',
|
|
1411
|
-
'}',
|
|
1412
|
-
'function runBestEffortKiller(command, args) {',
|
|
1413
|
-
' return new Promise((resolve) => {',
|
|
1414
|
-
' const killer = spawn(command, args, { stdio: payload.stdioMode, windowsHide: payload.stdioMode !== "inherit" });',
|
|
1415
|
-
" killer.once('error', () => resolve());",
|
|
1416
|
-
" killer.once('exit', () => resolve());",
|
|
1417
|
-
' });',
|
|
1418
|
-
'}',
|
|
1419
|
-
'async function forceKillRetiringProcess() {',
|
|
1420
|
-
' assertValidRetiringPid();',
|
|
1421
|
-
' try {',
|
|
1422
|
-
" process.kill(payload.retiringPid, 'SIGKILL');",
|
|
1423
|
-
' } catch (error) {',
|
|
1424
|
-
' const code = getErrorCode(error);',
|
|
1425
|
-
' if (code === "ESRCH") return;',
|
|
1426
|
-
' if (process.platform !== "win32") throw error;',
|
|
1427
|
-
' }',
|
|
1428
|
-
' if (process.platform === "win32") {',
|
|
1429
|
-
" await runBestEffortKiller('taskkill.exe', ['/PID', String(payload.retiringPid), '/F']);",
|
|
1430
|
-
' }',
|
|
1431
|
-
'}',
|
|
1432
|
-
'(async () => {',
|
|
1433
|
-
' try {',
|
|
1434
|
-
' const forceKillDeadline = Date.now() + payload.forceKillAfterMs;',
|
|
1435
|
-
' const exitedGracefully = await waitForRetiringProcessExit(payload.forceKillAfterMs);',
|
|
1436
|
-
' if (!exitedGracefully) {',
|
|
1437
|
-
' await forceKillRetiringProcess();',
|
|
1438
|
-
' const exitedAfterKill = await waitForRetiringProcessExit(payload.portReleaseTimeoutMs);',
|
|
1439
|
-
' if (!exitedAfterKill) {',
|
|
1440
|
-
' throw new Error(`Dominds retiring process is still alive after force-killing pid ${String(payload.retiringPid)}`);',
|
|
1441
|
-
' }',
|
|
1442
|
-
' }',
|
|
1443
|
-
' const portReleaseDeadline = Math.max(forceKillDeadline, Date.now() + payload.portReleaseTimeoutMs);',
|
|
1444
|
-
' const portReleased = await waitForPortReleaseUntil(portReleaseDeadline);',
|
|
1445
|
-
' if (!portReleased) {',
|
|
1446
|
-
' throw new Error(`Dominds restart port is still busy after retiring pid ${String(payload.retiringPid)} exited; port=${String(payload.host)}:${String(payload.port)}`);',
|
|
1447
|
-
' }',
|
|
1448
|
-
' const child = spawn(payload.command, payload.args, { cwd: payload.cwd, env: process.env, detached, stdio: payload.stdioMode, shell: false, windowsHide: payload.stdioMode !== "inherit" });',
|
|
1449
|
-
' if (detached) child.unref();',
|
|
1450
|
-
' await new Promise((resolve, reject) => {',
|
|
1451
|
-
" child.once('error', reject);",
|
|
1452
|
-
" child.once('spawn', resolve);",
|
|
1453
|
-
' });',
|
|
1454
|
-
' process.exit(0);',
|
|
1455
|
-
' } catch (error) {',
|
|
1456
|
-
' console.error(error instanceof Error ? error.message : String(error));',
|
|
1457
|
-
' process.exit(1);',
|
|
1458
|
-
' }',
|
|
1459
|
-
'})();',
|
|
1460
|
-
].join('\n');
|
|
1461
|
-
const helper = (0, child_process_1.spawn)(process.execPath, ['-e', helperScript, helperPayload], {
|
|
1400
|
+
traceFile: params.trace.traceFile,
|
|
1401
|
+
debugDir: params.trace.debugDir,
|
|
1402
|
+
};
|
|
1403
|
+
const helper = (0, child_process_1.spawn)(process.execPath, [helperEntrypoint, JSON.stringify(helperPayload)], {
|
|
1462
1404
|
cwd: params.cwd,
|
|
1463
1405
|
env: process.env,
|
|
1464
1406
|
detached: stdioMode !== 'inherit',
|
|
1465
1407
|
stdio: stdioMode,
|
|
1466
1408
|
windowsHide: stdioMode !== 'inherit',
|
|
1467
1409
|
});
|
|
1410
|
+
const helperPid = typeof helper.pid === 'number' ? helper.pid : null;
|
|
1411
|
+
const helperSpawned = new Promise((resolve, reject) => {
|
|
1412
|
+
helper.once('spawn', () => {
|
|
1413
|
+
appendRestartTraceSoon(params.trace, 'parent.helper_spawn_event', {
|
|
1414
|
+
helperPid,
|
|
1415
|
+
});
|
|
1416
|
+
resolve(helperPid);
|
|
1417
|
+
});
|
|
1418
|
+
helper.once('error', (error) => {
|
|
1419
|
+
appendRestartTraceSoon(params.trace, 'parent.helper_error_event', {
|
|
1420
|
+
message: error.message,
|
|
1421
|
+
stack: error.stack ?? null,
|
|
1422
|
+
});
|
|
1423
|
+
reject(error);
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
helper.once('exit', (code, signal) => {
|
|
1427
|
+
appendRestartTraceSoon(params.trace, 'parent.helper_exit_event', {
|
|
1428
|
+
code,
|
|
1429
|
+
signal,
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1468
1432
|
if (stdioMode !== 'inherit') {
|
|
1469
1433
|
helper.unref();
|
|
1470
1434
|
}
|
|
1435
|
+
return await helperSpawned;
|
|
1471
1436
|
}
|
|
1472
1437
|
async function stopAndExitForRestart() {
|
|
1473
1438
|
const cfg = assertRuntimeConfig();
|
|
1439
|
+
const trace = activeRestartTraceFile === null
|
|
1440
|
+
? null
|
|
1441
|
+
: { debugDir: path_1.default.dirname(activeRestartTraceFile), traceFile: activeRestartTraceFile };
|
|
1474
1442
|
let stopSettled = false;
|
|
1475
|
-
|
|
1443
|
+
if (trace !== null) {
|
|
1444
|
+
await appendRestartTraceBestEffort(trace, 'parent.stop_and_exit.start', {
|
|
1445
|
+
host: cfg.host,
|
|
1446
|
+
port: cfg.port,
|
|
1447
|
+
exitGraceMs: RESTART_EXIT_GRACE_MS,
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
const stopPromise = cfg
|
|
1476
1451
|
.stopServer()
|
|
1477
1452
|
.then(() => {
|
|
1478
1453
|
stopSettled = true;
|
|
1454
|
+
if (trace !== null) {
|
|
1455
|
+
return appendRestartTraceBestEffort(trace, 'parent.stop_server.finish', {
|
|
1456
|
+
host: cfg.host,
|
|
1457
|
+
port: cfg.port,
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
return undefined;
|
|
1479
1461
|
})
|
|
1480
1462
|
.catch((error) => {
|
|
1481
1463
|
stopSettled = true;
|
|
1464
|
+
if (trace !== null) {
|
|
1465
|
+
return appendRestartTraceBestEffort(trace, 'parent.stop_server.error', {
|
|
1466
|
+
host: cfg.host,
|
|
1467
|
+
port: cfg.port,
|
|
1468
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1469
|
+
stack: error instanceof Error ? (error.stack ?? null) : null,
|
|
1470
|
+
}).then(() => {
|
|
1471
|
+
log.error('Failed to stop Dominds HTTP server during restart grace window', error, {
|
|
1472
|
+
host: cfg.host,
|
|
1473
|
+
port: cfg.port,
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1482
1477
|
log.error('Failed to stop Dominds HTTP server during restart grace window', error, {
|
|
1483
1478
|
host: cfg.host,
|
|
1484
1479
|
port: cfg.port,
|
|
1485
1480
|
});
|
|
1481
|
+
return undefined;
|
|
1486
1482
|
});
|
|
1487
1483
|
cfg.closeWebSocketClients();
|
|
1488
1484
|
await delayMs(RESTART_EXIT_GRACE_MS);
|
|
@@ -1493,6 +1489,17 @@ async function stopAndExitForRestart() {
|
|
|
1493
1489
|
graceMs: RESTART_EXIT_GRACE_MS,
|
|
1494
1490
|
});
|
|
1495
1491
|
}
|
|
1492
|
+
if (trace !== null) {
|
|
1493
|
+
if (stopSettled) {
|
|
1494
|
+
await stopPromise;
|
|
1495
|
+
}
|
|
1496
|
+
await appendRestartTraceBestEffort(trace, 'parent.process_exit', {
|
|
1497
|
+
host: cfg.host,
|
|
1498
|
+
port: cfg.port,
|
|
1499
|
+
stopSettled,
|
|
1500
|
+
code: 0,
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1496
1503
|
process.exit(0);
|
|
1497
1504
|
}
|
|
1498
1505
|
async function restartDomindsIntoLatest() {
|
|
@@ -1529,24 +1536,59 @@ async function restartDomindsIntoLatest() {
|
|
|
1529
1536
|
}
|
|
1530
1537
|
restartState = { kind: 'restarting', targetVersion: status.targetVersion };
|
|
1531
1538
|
publishStatusUpdateSoon();
|
|
1539
|
+
const restartCwd = process.cwd();
|
|
1540
|
+
const trace = buildRestartTraceContext();
|
|
1541
|
+
activeRestartTraceFile = trace.traceFile;
|
|
1532
1542
|
try {
|
|
1533
|
-
|
|
1543
|
+
await appendRestartTraceBestEffort(trace, 'parent.restart_requested', {
|
|
1544
|
+
runKind,
|
|
1545
|
+
currentVersion: status.currentVersion,
|
|
1546
|
+
targetVersion: status.targetVersion,
|
|
1547
|
+
launchCommand: launchSpec.command,
|
|
1548
|
+
launchArgs: launchSpec.args,
|
|
1549
|
+
restartCwd,
|
|
1550
|
+
host: cfg.host,
|
|
1551
|
+
port: cfg.port,
|
|
1552
|
+
traceFile: trace.traceFile,
|
|
1553
|
+
});
|
|
1554
|
+
const helperPid = await spawnDetachedRestartHelper({
|
|
1534
1555
|
command: launchSpec.command,
|
|
1535
1556
|
args: launchSpec.args,
|
|
1536
|
-
cwd:
|
|
1557
|
+
cwd: restartCwd,
|
|
1558
|
+
host: cfg.host,
|
|
1559
|
+
port: cfg.port,
|
|
1560
|
+
trace,
|
|
1561
|
+
});
|
|
1562
|
+
await appendRestartTraceBestEffort(trace, 'parent.helper_spawned', {
|
|
1563
|
+
helperPid,
|
|
1564
|
+
retiringPid: process.pid,
|
|
1537
1565
|
host: cfg.host,
|
|
1538
1566
|
port: cfg.port,
|
|
1539
1567
|
});
|
|
1568
|
+
log.info('Dominds restart helper spawned', undefined, {
|
|
1569
|
+
helperPid,
|
|
1570
|
+
traceFile: trace.traceFile,
|
|
1571
|
+
});
|
|
1540
1572
|
setImmediate(() => {
|
|
1541
1573
|
void stopAndExitForRestart().catch((error) => {
|
|
1574
|
+
void appendRestartTraceBestEffort(trace, 'parent.stop_and_exit.error', {
|
|
1575
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1576
|
+
stack: error instanceof Error ? (error.stack ?? null) : null,
|
|
1577
|
+
});
|
|
1542
1578
|
log.error('Failed to stop Dominds server during restart', error);
|
|
1543
1579
|
restartState = previousRestartRequiredState ?? IDLE_RESTART_STATE;
|
|
1580
|
+
activeRestartTraceFile = null;
|
|
1544
1581
|
publishStatusUpdateSoon();
|
|
1545
1582
|
});
|
|
1546
1583
|
});
|
|
1547
1584
|
}
|
|
1548
1585
|
catch (error) {
|
|
1586
|
+
await appendRestartTraceBestEffort(trace, 'parent.restart_error', {
|
|
1587
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1588
|
+
stack: error instanceof Error ? (error.stack ?? null) : null,
|
|
1589
|
+
});
|
|
1549
1590
|
restartState = previousRestartRequiredState ?? IDLE_RESTART_STATE;
|
|
1591
|
+
activeRestartTraceFile = null;
|
|
1550
1592
|
publishStatusUpdateSoon();
|
|
1551
1593
|
throw error;
|
|
1552
1594
|
}
|
package/dist/server.js
CHANGED
|
@@ -287,13 +287,14 @@ async function main() {
|
|
|
287
287
|
// Handle working directory change from -C flag
|
|
288
288
|
const wsDir = cliArgs['C'];
|
|
289
289
|
if (wsDir) {
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
if (!path.isAbsolute(wsDir)) {
|
|
291
|
+
throw new Error(`-C requires an absolute directory path: ${wsDir}`);
|
|
292
|
+
}
|
|
292
293
|
try {
|
|
293
294
|
process.chdir(wsDir);
|
|
294
295
|
}
|
|
295
296
|
catch (err) {
|
|
296
|
-
|
|
297
|
+
throw new Error(`Failed to change working directory to ${wsDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
// Get port, host, and mode from CLI args
|