dominds 1.25.11 → 1.25.13
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/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/access-control.js +9 -5
- package/dist/docs/team_mgmt-toolset.md +1 -1
- package/dist/docs/team_mgmt-toolset.zh.md +1 -1
- package/dist/llm/kernel-driver/sideDialog.d.ts +1 -0
- package/dist/llm/kernel-driver/sideDialog.js +29 -11
- package/dist/llm/kernel-driver/tellask-special.js +3 -0
- package/dist/persistence.d.ts +1 -0
- package/dist/persistence.js +23 -11
- package/dist/server/api-routes.js +65 -0
- package/dist/server/dialog-forensics-routes.d.ts +2 -0
- package/dist/server/dialog-forensics-routes.js +549 -0
- package/dist/server/dominds-self-update.js +54 -31
- package/dist/tools/builtins.js +14 -16
- package/dist/tools/os.d.ts +14 -0
- package/dist/tools/os.js +508 -101
- package/dist/tools/prompts/codex_inspect_and_patch_tools/en/tools.md +2 -1
- package/dist/tools/prompts/codex_inspect_and_patch_tools/zh/tools.md +2 -1
- package/package.json +3 -3
- package/webapp/dist/assets/{_basePickBy-BF9Zg9uq.js → _basePickBy-B1-brAPU.js} +3 -3
- package/webapp/dist/assets/{_basePickBy-BF9Zg9uq.js.map → _basePickBy-B1-brAPU.js.map} +1 -1
- package/webapp/dist/assets/{_baseUniq-CFjISsgz.js → _baseUniq-BuB0jmKD.js} +2 -2
- package/webapp/dist/assets/{_baseUniq-CFjISsgz.js.map → _baseUniq-BuB0jmKD.js.map} +1 -1
- package/webapp/dist/assets/{arc-BVyYGzE7.js → arc-CvsBvjnB.js} +2 -2
- package/webapp/dist/assets/{arc-BVyYGzE7.js.map → arc-CvsBvjnB.js.map} +1 -1
- package/webapp/dist/assets/{architectureDiagram-2XIMDMQ5-SEmNTU1b.js → architectureDiagram-2XIMDMQ5-B-xGuLbb.js} +7 -7
- package/webapp/dist/assets/{architectureDiagram-2XIMDMQ5-SEmNTU1b.js.map → architectureDiagram-2XIMDMQ5-B-xGuLbb.js.map} +1 -1
- package/webapp/dist/assets/{blockDiagram-WCTKOSBZ-BndD4gLF.js → blockDiagram-WCTKOSBZ-Bu53fwsa.js} +7 -7
- package/webapp/dist/assets/{blockDiagram-WCTKOSBZ-BndD4gLF.js.map → blockDiagram-WCTKOSBZ-Bu53fwsa.js.map} +1 -1
- package/webapp/dist/assets/{c4Diagram-IC4MRINW-fGAz7umu.js → c4Diagram-IC4MRINW-D9-FJ4LB.js} +3 -3
- package/webapp/dist/assets/{c4Diagram-IC4MRINW-fGAz7umu.js.map → c4Diagram-IC4MRINW-D9-FJ4LB.js.map} +1 -1
- package/webapp/dist/assets/{channel-Blt7S1Sn.js → channel-D1VPurpu.js} +2 -2
- package/webapp/dist/assets/{channel-Blt7S1Sn.js.map → channel-D1VPurpu.js.map} +1 -1
- package/webapp/dist/assets/{chunk-4BX2VUAB-C2FKcyob.js → chunk-4BX2VUAB-DB14wcWS.js} +2 -2
- package/webapp/dist/assets/{chunk-4BX2VUAB-C2FKcyob.js.map → chunk-4BX2VUAB-DB14wcWS.js.map} +1 -1
- package/webapp/dist/assets/{chunk-55IACEB6-CN8ZmdUP.js → chunk-55IACEB6-C5KCE85A.js} +2 -2
- package/webapp/dist/assets/{chunk-55IACEB6-CN8ZmdUP.js.map → chunk-55IACEB6-C5KCE85A.js.map} +1 -1
- package/webapp/dist/assets/{chunk-FMBD7UC4-B9Uq2tt2.js → chunk-FMBD7UC4-Bb6zm0iH.js} +2 -2
- package/webapp/dist/assets/{chunk-FMBD7UC4-B9Uq2tt2.js.map → chunk-FMBD7UC4-Bb6zm0iH.js.map} +1 -1
- package/webapp/dist/assets/{chunk-JSJVCQXG-vVrXi8LV.js → chunk-JSJVCQXG-CJPrv6fM.js} +2 -2
- package/webapp/dist/assets/{chunk-JSJVCQXG-vVrXi8LV.js.map → chunk-JSJVCQXG-CJPrv6fM.js.map} +1 -1
- package/webapp/dist/assets/{chunk-KX2RTZJC-DtZmdBq3.js → chunk-KX2RTZJC-8J1Swk7E.js} +2 -2
- package/webapp/dist/assets/{chunk-KX2RTZJC-DtZmdBq3.js.map → chunk-KX2RTZJC-8J1Swk7E.js.map} +1 -1
- package/webapp/dist/assets/{chunk-NQ4KR5QH-C3rU1XEw.js → chunk-NQ4KR5QH-DySHY4x9.js} +4 -4
- package/webapp/dist/assets/{chunk-NQ4KR5QH-C3rU1XEw.js.map → chunk-NQ4KR5QH-DySHY4x9.js.map} +1 -1
- package/webapp/dist/assets/{chunk-QZHKN3VN-CeHQK_vs.js → chunk-QZHKN3VN-CTO5Z-4P.js} +2 -2
- package/webapp/dist/assets/{chunk-QZHKN3VN-CeHQK_vs.js.map → chunk-QZHKN3VN-CTO5Z-4P.js.map} +1 -1
- package/webapp/dist/assets/{chunk-WL4C6EOR-Bb8GUwSo.js → chunk-WL4C6EOR-Ci_Ec8Ax.js} +6 -6
- package/webapp/dist/assets/{chunk-WL4C6EOR-Bb8GUwSo.js.map → chunk-WL4C6EOR-Ci_Ec8Ax.js.map} +1 -1
- package/webapp/dist/assets/{classDiagram-VBA2DB6C-CmP7X8Fj.js → classDiagram-VBA2DB6C-CxAxBvv7.js} +7 -7
- package/webapp/dist/assets/{classDiagram-VBA2DB6C-CmP7X8Fj.js.map → classDiagram-VBA2DB6C-CxAxBvv7.js.map} +1 -1
- package/webapp/dist/assets/{classDiagram-v2-RAHNMMFH-CmP7X8Fj.js → classDiagram-v2-RAHNMMFH-CxAxBvv7.js} +7 -7
- package/webapp/dist/assets/{classDiagram-v2-RAHNMMFH-CmP7X8Fj.js.map → classDiagram-v2-RAHNMMFH-CxAxBvv7.js.map} +1 -1
- package/webapp/dist/assets/{clone-CzGKO47U.js → clone-ePiNaiNY.js} +2 -2
- package/webapp/dist/assets/{clone-CzGKO47U.js.map → clone-ePiNaiNY.js.map} +1 -1
- package/webapp/dist/assets/{cose-bilkent-S5V4N54A-3wbordhk.js → cose-bilkent-S5V4N54A-j-Ex-Sef.js} +2 -2
- package/webapp/dist/assets/{cose-bilkent-S5V4N54A-3wbordhk.js.map → cose-bilkent-S5V4N54A-j-Ex-Sef.js.map} +1 -1
- package/webapp/dist/assets/{dagre-KLK3FWXG-DrM7hFR5.js → dagre-KLK3FWXG-ihZ2wOCM.js} +7 -7
- package/webapp/dist/assets/{dagre-KLK3FWXG-DrM7hFR5.js.map → dagre-KLK3FWXG-ihZ2wOCM.js.map} +1 -1
- package/webapp/dist/assets/{diagram-E7M64L7V-D-bQEVk2.js → diagram-E7M64L7V-Cp4GQGS7.js} +8 -8
- package/webapp/dist/assets/{diagram-E7M64L7V-D-bQEVk2.js.map → diagram-E7M64L7V-Cp4GQGS7.js.map} +1 -1
- package/webapp/dist/assets/{diagram-IFDJBPK2-MaU_xQfR.js → diagram-IFDJBPK2-B70cgyS5.js} +7 -7
- package/webapp/dist/assets/{diagram-IFDJBPK2-MaU_xQfR.js.map → diagram-IFDJBPK2-B70cgyS5.js.map} +1 -1
- package/webapp/dist/assets/{diagram-P4PSJMXO-BLklmc9h.js → diagram-P4PSJMXO-DMOv7eKE.js} +7 -7
- package/webapp/dist/assets/{diagram-P4PSJMXO-BLklmc9h.js.map → diagram-P4PSJMXO-DMOv7eKE.js.map} +1 -1
- package/webapp/dist/assets/{erDiagram-INFDFZHY-DHIVFsNv.js → erDiagram-INFDFZHY-BKpXWjIc.js} +5 -5
- package/webapp/dist/assets/{erDiagram-INFDFZHY-DHIVFsNv.js.map → erDiagram-INFDFZHY-BKpXWjIc.js.map} +1 -1
- package/webapp/dist/assets/{flowDiagram-PKNHOUZH-HbMxSdg1.js → flowDiagram-PKNHOUZH-DgrItj0h.js} +7 -7
- package/webapp/dist/assets/{flowDiagram-PKNHOUZH-HbMxSdg1.js.map → flowDiagram-PKNHOUZH-DgrItj0h.js.map} +1 -1
- package/webapp/dist/assets/{ganttDiagram-A5KZAMGK-CxBETPNC.js → ganttDiagram-A5KZAMGK-7-8hlYsT.js} +3 -3
- package/webapp/dist/assets/{ganttDiagram-A5KZAMGK-CxBETPNC.js.map → ganttDiagram-A5KZAMGK-7-8hlYsT.js.map} +1 -1
- package/webapp/dist/assets/{gitGraphDiagram-K3NZZRJ6-DPV-fFTC.js → gitGraphDiagram-K3NZZRJ6-cPSaCUUk.js} +8 -8
- package/webapp/dist/assets/{gitGraphDiagram-K3NZZRJ6-DPV-fFTC.js.map → gitGraphDiagram-K3NZZRJ6-cPSaCUUk.js.map} +1 -1
- package/webapp/dist/assets/{graph-C0MlCXJg.js → graph-CAlg3tEk.js} +3 -3
- package/webapp/dist/assets/{graph-C0MlCXJg.js.map → graph-CAlg3tEk.js.map} +1 -1
- package/webapp/dist/assets/{index-CzHjX_nj.js → index-DLTS_eOh.js} +123 -47
- package/webapp/dist/assets/{index-CzHjX_nj.js.map → index-DLTS_eOh.js.map} +1 -1
- package/webapp/dist/assets/{infoDiagram-LFFYTUFH-ChTC2kD-.js → infoDiagram-LFFYTUFH-CHJHvxMC.js} +6 -6
- package/webapp/dist/assets/{infoDiagram-LFFYTUFH-ChTC2kD-.js.map → infoDiagram-LFFYTUFH-CHJHvxMC.js.map} +1 -1
- package/webapp/dist/assets/{ishikawaDiagram-PHBUUO56--aJd3LM6.js → ishikawaDiagram-PHBUUO56-S8N-XZ8E.js} +2 -2
- package/webapp/dist/assets/{ishikawaDiagram-PHBUUO56--aJd3LM6.js.map → ishikawaDiagram-PHBUUO56-S8N-XZ8E.js.map} +1 -1
- package/webapp/dist/assets/{journeyDiagram-4ABVD52K-Bzd3cZTs.js → journeyDiagram-4ABVD52K-ChHNpMtH.js} +5 -5
- package/webapp/dist/assets/{journeyDiagram-4ABVD52K-Bzd3cZTs.js.map → journeyDiagram-4ABVD52K-ChHNpMtH.js.map} +1 -1
- package/webapp/dist/assets/{kanban-definition-K7BYSVSG-DiFHcs58.js → kanban-definition-K7BYSVSG-Cqxd99wZ.js} +3 -3
- package/webapp/dist/assets/{kanban-definition-K7BYSVSG-DiFHcs58.js.map → kanban-definition-K7BYSVSG-Cqxd99wZ.js.map} +1 -1
- package/webapp/dist/assets/{layout-B9Hsf17G.js → layout-uOLcVthp.js} +5 -5
- package/webapp/dist/assets/{layout-B9Hsf17G.js.map → layout-uOLcVthp.js.map} +1 -1
- package/webapp/dist/assets/{linear-6xqU78Yu.js → linear-Ga_f4H_w.js} +2 -2
- package/webapp/dist/assets/{linear-6xqU78Yu.js.map → linear-Ga_f4H_w.js.map} +1 -1
- package/webapp/dist/assets/{mindmap-definition-YRQLILUH-BUI6M5up.js → mindmap-definition-YRQLILUH-TSH7wOlZ.js} +4 -4
- package/webapp/dist/assets/{mindmap-definition-YRQLILUH-BUI6M5up.js.map → mindmap-definition-YRQLILUH-TSH7wOlZ.js.map} +1 -1
- package/webapp/dist/assets/{pieDiagram-SKSYHLDU-FCGp31Lg.js → pieDiagram-SKSYHLDU-DPXszqns.js} +8 -8
- package/webapp/dist/assets/{pieDiagram-SKSYHLDU-FCGp31Lg.js.map → pieDiagram-SKSYHLDU-DPXszqns.js.map} +1 -1
- package/webapp/dist/assets/{quadrantDiagram-337W2JSQ-bJcMGXM6.js → quadrantDiagram-337W2JSQ-BgA_GVhR.js} +3 -3
- package/webapp/dist/assets/{quadrantDiagram-337W2JSQ-bJcMGXM6.js.map → quadrantDiagram-337W2JSQ-BgA_GVhR.js.map} +1 -1
- package/webapp/dist/assets/{requirementDiagram-Z7DCOOCP-BghuL9nW.js → requirementDiagram-Z7DCOOCP-CM-47daj.js} +4 -4
- package/webapp/dist/assets/{requirementDiagram-Z7DCOOCP-BghuL9nW.js.map → requirementDiagram-Z7DCOOCP-CM-47daj.js.map} +1 -1
- package/webapp/dist/assets/{sankeyDiagram-WA2Y5GQK-DpuFpv6d.js → sankeyDiagram-WA2Y5GQK-CxDCwHAj.js} +2 -2
- package/webapp/dist/assets/{sankeyDiagram-WA2Y5GQK-DpuFpv6d.js.map → sankeyDiagram-WA2Y5GQK-CxDCwHAj.js.map} +1 -1
- package/webapp/dist/assets/{sequenceDiagram-2WXFIKYE-BM6rPANl.js → sequenceDiagram-2WXFIKYE-1UJP6Fff.js} +4 -4
- package/webapp/dist/assets/{sequenceDiagram-2WXFIKYE-BM6rPANl.js.map → sequenceDiagram-2WXFIKYE-1UJP6Fff.js.map} +1 -1
- package/webapp/dist/assets/{stateDiagram-RAJIS63D-C_jQSre0.js → stateDiagram-RAJIS63D-B2aqr0KQ.js} +9 -9
- package/webapp/dist/assets/{stateDiagram-RAJIS63D-C_jQSre0.js.map → stateDiagram-RAJIS63D-B2aqr0KQ.js.map} +1 -1
- package/webapp/dist/assets/{stateDiagram-v2-FVOUBMTO-BbQxj-LI.js → stateDiagram-v2-FVOUBMTO-DvmkevVb.js} +5 -5
- package/webapp/dist/assets/{stateDiagram-v2-FVOUBMTO-BbQxj-LI.js.map → stateDiagram-v2-FVOUBMTO-DvmkevVb.js.map} +1 -1
- package/webapp/dist/assets/{timeline-definition-YZTLITO2-qVPiYzDY.js → timeline-definition-YZTLITO2-CaOrqzT1.js} +3 -3
- package/webapp/dist/assets/{timeline-definition-YZTLITO2-qVPiYzDY.js.map → timeline-definition-YZTLITO2-CaOrqzT1.js.map} +1 -1
- package/webapp/dist/assets/{treemap-KZPCXAKY-CH_Gjw5E.js → treemap-KZPCXAKY-CWs_8GJm.js} +5 -5
- package/webapp/dist/assets/{treemap-KZPCXAKY-CH_Gjw5E.js.map → treemap-KZPCXAKY-CWs_8GJm.js.map} +1 -1
- package/webapp/dist/assets/{vennDiagram-LZ73GAT5-rpOhNWvx.js → vennDiagram-LZ73GAT5-BuBxFDz6.js} +2 -2
- package/webapp/dist/assets/{vennDiagram-LZ73GAT5-rpOhNWvx.js.map → vennDiagram-LZ73GAT5-BuBxFDz6.js.map} +1 -1
- package/webapp/dist/assets/{xychartDiagram-JWTSCODW-D1TcBJrI.js → xychartDiagram-JWTSCODW-ChjL-_2b.js} +3 -3
- package/webapp/dist/assets/{xychartDiagram-JWTSCODW-D1TcBJrI.js.map → xychartDiagram-JWTSCODW-ChjL-_2b.js.map} +1 -1
- package/webapp/dist/index.html +1 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.handleDialogForensicsZipRoute = handleDialogForensicsZipRoute;
|
|
40
|
+
const time_1 = require("@longrun-ai/kernel/utils/time");
|
|
41
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const FORENSICS_STATUS_DIRS = ['run', 'done', 'archive', 'malformed'];
|
|
44
|
+
class BadForensicsRequestError extends Error {
|
|
45
|
+
}
|
|
46
|
+
class ForensicsNotFoundError extends Error {
|
|
47
|
+
}
|
|
48
|
+
function badRequest(message) {
|
|
49
|
+
throw new BadForensicsRequestError(message);
|
|
50
|
+
}
|
|
51
|
+
function notFound(message) {
|
|
52
|
+
throw new ForensicsNotFoundError(message);
|
|
53
|
+
}
|
|
54
|
+
const ROOT_DIAGNOSTIC_FILE_NAMES = [
|
|
55
|
+
'latest.yaml',
|
|
56
|
+
'dialog.yaml',
|
|
57
|
+
'drive-watch.json',
|
|
58
|
+
'active-callees.json',
|
|
59
|
+
'sideDialog-responses.json',
|
|
60
|
+
'q4h.yaml',
|
|
61
|
+
'backend-drive-stalls.jsonl',
|
|
62
|
+
'wake-queue.jsonl',
|
|
63
|
+
'asker-stack.jsonl',
|
|
64
|
+
];
|
|
65
|
+
const CRC32_TABLE = (() => {
|
|
66
|
+
const table = [];
|
|
67
|
+
for (let i = 0; i < 256; i += 1) {
|
|
68
|
+
let value = i;
|
|
69
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
70
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
71
|
+
}
|
|
72
|
+
table.push(value >>> 0);
|
|
73
|
+
}
|
|
74
|
+
return table;
|
|
75
|
+
})();
|
|
76
|
+
function respondJson(res, statusCode, data) {
|
|
77
|
+
res.writeHead(statusCode, {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
'Cache-Control': 'no-store',
|
|
80
|
+
});
|
|
81
|
+
res.end(JSON.stringify(data));
|
|
82
|
+
}
|
|
83
|
+
function isPositiveIntegerText(raw) {
|
|
84
|
+
if (raw === '')
|
|
85
|
+
return false;
|
|
86
|
+
for (const char of raw) {
|
|
87
|
+
if (char < '0' || char > '9')
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function parsePositiveInteger(raw, fieldName) {
|
|
93
|
+
if (raw === null || raw.trim() === '')
|
|
94
|
+
return undefined;
|
|
95
|
+
if (!isPositiveIntegerText(raw)) {
|
|
96
|
+
badRequest(`${fieldName} must be a positive integer`);
|
|
97
|
+
}
|
|
98
|
+
const value = Number(raw);
|
|
99
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
100
|
+
badRequest(`${fieldName} must be a positive integer`);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
function normalizeStatus(raw) {
|
|
105
|
+
if (raw === null || raw.trim() === '')
|
|
106
|
+
return undefined;
|
|
107
|
+
switch (raw) {
|
|
108
|
+
case 'run':
|
|
109
|
+
case 'running':
|
|
110
|
+
return 'run';
|
|
111
|
+
case 'done':
|
|
112
|
+
case 'completed':
|
|
113
|
+
return 'done';
|
|
114
|
+
case 'archive':
|
|
115
|
+
case 'archived':
|
|
116
|
+
return 'archive';
|
|
117
|
+
case 'malformed':
|
|
118
|
+
return 'malformed';
|
|
119
|
+
default:
|
|
120
|
+
badRequest('status must be one of run, running, done, completed, archive, archived, malformed');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function normalizeMode(raw) {
|
|
124
|
+
if (raw === null || raw.trim() === '')
|
|
125
|
+
return 'full';
|
|
126
|
+
if (raw === 'full' || raw === 'pick')
|
|
127
|
+
return raw;
|
|
128
|
+
badRequest('mode must be full or pick');
|
|
129
|
+
}
|
|
130
|
+
function hasTraversalSegment(value) {
|
|
131
|
+
const parts = value.split('/');
|
|
132
|
+
return parts.some((part) => part === '' || part === '.' || part === '..');
|
|
133
|
+
}
|
|
134
|
+
function parseDialogId(raw, fieldName) {
|
|
135
|
+
if (raw === null || raw.trim() === '') {
|
|
136
|
+
badRequest(`${fieldName} is required`);
|
|
137
|
+
}
|
|
138
|
+
if (raw.includes(String.fromCharCode(92)) || raw.includes(String.fromCharCode(0))) {
|
|
139
|
+
badRequest(`${fieldName} must be a slash-separated relative dialog id`);
|
|
140
|
+
}
|
|
141
|
+
if (path.isAbsolute(raw) || hasTraversalSegment(raw)) {
|
|
142
|
+
badRequest(`${fieldName} must not contain empty or traversal path segments`);
|
|
143
|
+
}
|
|
144
|
+
return raw;
|
|
145
|
+
}
|
|
146
|
+
function validateBundleEntryName(name, fieldName) {
|
|
147
|
+
if (name === '' ||
|
|
148
|
+
name.startsWith('/') ||
|
|
149
|
+
name.includes(String.fromCharCode(92)) ||
|
|
150
|
+
name.includes(String.fromCharCode(0)) ||
|
|
151
|
+
hasTraversalSegment(name)) {
|
|
152
|
+
badRequest(`${fieldName} must be a safe bundle entry path`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function parseRepeatedSafeBundlePaths(requestUrl, queryName, fieldName) {
|
|
156
|
+
const paths = requestUrl.searchParams
|
|
157
|
+
.getAll(queryName)
|
|
158
|
+
.map((raw) => raw.trim().replace(/\/+$/, ''))
|
|
159
|
+
.filter((raw) => raw !== '');
|
|
160
|
+
const unique = [...new Set(paths)];
|
|
161
|
+
for (const item of unique) {
|
|
162
|
+
validateBundleEntryName(item, fieldName);
|
|
163
|
+
}
|
|
164
|
+
return unique;
|
|
165
|
+
}
|
|
166
|
+
function parseFileSelection(requestUrl) {
|
|
167
|
+
return parseRepeatedSafeBundlePaths(requestUrl, 'files', 'files');
|
|
168
|
+
}
|
|
169
|
+
function parseForensicsLocatorRequest(requestUrl) {
|
|
170
|
+
const rootId = parseDialogId(requestUrl.searchParams.get('rootId'), 'rootId');
|
|
171
|
+
const selfId = parseDialogId(requestUrl.searchParams.get('selfId') ?? rootId, 'selfId');
|
|
172
|
+
return {
|
|
173
|
+
rootId,
|
|
174
|
+
selfId,
|
|
175
|
+
requestedStatus: normalizeStatus(requestUrl.searchParams.get('status')),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function parseForensicsRequest(requestUrl) {
|
|
179
|
+
const locator = parseForensicsLocatorRequest(requestUrl);
|
|
180
|
+
const mode = normalizeMode(requestUrl.searchParams.get('mode'));
|
|
181
|
+
const files = parseFileSelection(requestUrl);
|
|
182
|
+
if (mode === 'pick' && files.length === 0) {
|
|
183
|
+
badRequest('mode=pick requires at least one files= parameter');
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
...locator,
|
|
187
|
+
course: parsePositiveInteger(requestUrl.searchParams.get('course'), 'course'),
|
|
188
|
+
mode,
|
|
189
|
+
files,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function isNodeFileNotFound(error) {
|
|
193
|
+
return (typeof error === 'object' && error !== null && error.code === 'ENOENT');
|
|
194
|
+
}
|
|
195
|
+
async function pathExists(pathAbs) {
|
|
196
|
+
try {
|
|
197
|
+
await promises_1.default.access(pathAbs);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (isNodeFileNotFound(error))
|
|
202
|
+
return false;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function resolveForensicsPaths(request) {
|
|
207
|
+
const dialogsRoot = path.join(process.cwd(), '.dialogs');
|
|
208
|
+
const statuses = request.requestedStatus ? [request.requestedStatus] : FORENSICS_STATUS_DIRS;
|
|
209
|
+
const targetKind = request.selfId === request.rootId ? 'root' : 'side';
|
|
210
|
+
for (const status of statuses) {
|
|
211
|
+
const rootPath = path.join(dialogsRoot, status, request.rootId);
|
|
212
|
+
const targetPath = targetKind === 'root' ? rootPath : path.join(rootPath, 'sideDialogs', request.selfId);
|
|
213
|
+
if (await pathExists(targetPath)) {
|
|
214
|
+
return { dialogsRoot, status, rootPath, targetPath, targetKind };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
notFound(`dialog records not found for rootId=${request.rootId}, selfId=${request.selfId}, status=${request.requestedStatus ?? 'auto'}`);
|
|
218
|
+
}
|
|
219
|
+
function toZipEntryName(...parts) {
|
|
220
|
+
let joined = parts.join('/').split(String.fromCharCode(92)).join('/');
|
|
221
|
+
while (joined.startsWith('/'))
|
|
222
|
+
joined = joined.slice(1);
|
|
223
|
+
while (joined.includes('//'))
|
|
224
|
+
joined = joined.replace('//', '/');
|
|
225
|
+
validateBundleEntryName(joined, 'zip entry');
|
|
226
|
+
return joined;
|
|
227
|
+
}
|
|
228
|
+
function sourcePathForBundleEntry(resolved, entryName) {
|
|
229
|
+
validateBundleEntryName(entryName, 'file');
|
|
230
|
+
const [prefix, ...restParts] = entryName.split('/');
|
|
231
|
+
if (restParts.length === 0) {
|
|
232
|
+
badRequest(`files must include a path after ${prefix}`);
|
|
233
|
+
}
|
|
234
|
+
const relativePath = path.join(...restParts);
|
|
235
|
+
switch (prefix) {
|
|
236
|
+
case 'side':
|
|
237
|
+
if (resolved.targetKind !== 'side') {
|
|
238
|
+
badRequest('side/* files can only be selected for a side dialog');
|
|
239
|
+
}
|
|
240
|
+
return path.join(resolved.targetPath, relativePath);
|
|
241
|
+
case 'root':
|
|
242
|
+
if (resolved.targetKind !== 'side') {
|
|
243
|
+
badRequest('root/* files can only be selected when selfId is a side dialog');
|
|
244
|
+
}
|
|
245
|
+
return path.join(resolved.rootPath, relativePath);
|
|
246
|
+
case 'dialog':
|
|
247
|
+
if (resolved.targetKind !== 'root') {
|
|
248
|
+
badRequest('dialog/* files can only be selected when selfId equals rootId');
|
|
249
|
+
}
|
|
250
|
+
return path.join(resolved.rootPath, relativePath);
|
|
251
|
+
case 'debug':
|
|
252
|
+
if (restParts.length !== 1) {
|
|
253
|
+
badRequest('debug selections must be direct debug filenames');
|
|
254
|
+
}
|
|
255
|
+
return path.join(resolved.dialogsRoot, 'debug', restParts[0]);
|
|
256
|
+
default:
|
|
257
|
+
badRequest('files must start with side/, root/, dialog/, or debug/');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function collectFileIfPresent(entries, missingFiles, sourcePath, entryName) {
|
|
261
|
+
try {
|
|
262
|
+
const stat = await promises_1.default.stat(sourcePath);
|
|
263
|
+
if (!stat.isFile()) {
|
|
264
|
+
missingFiles.push({ entryName, sourcePath });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
entries.push({
|
|
268
|
+
name: entryName,
|
|
269
|
+
data: await promises_1.default.readFile(sourcePath),
|
|
270
|
+
sourcePath,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (isNodeFileNotFound(error)) {
|
|
275
|
+
missingFiles.push({ entryName, sourcePath });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function collectTree(entries, rootPath, entryPrefix, relativeDir = '') {
|
|
282
|
+
const dirPath = path.join(rootPath, relativeDir);
|
|
283
|
+
const dirents = await promises_1.default.readdir(dirPath, { withFileTypes: true });
|
|
284
|
+
dirents.sort((left, right) => left.name.localeCompare(right.name));
|
|
285
|
+
for (const dirent of dirents) {
|
|
286
|
+
if (dirent.name === '.' || dirent.name === '..')
|
|
287
|
+
continue;
|
|
288
|
+
const nextRelative = relativeDir === '' ? dirent.name : path.join(relativeDir, dirent.name);
|
|
289
|
+
const sourcePath = path.join(rootPath, nextRelative);
|
|
290
|
+
if (dirent.isDirectory()) {
|
|
291
|
+
await collectTree(entries, rootPath, entryPrefix, nextRelative);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (dirent.isFile()) {
|
|
295
|
+
entries.push({
|
|
296
|
+
name: toZipEntryName(entryPrefix, nextRelative),
|
|
297
|
+
data: await promises_1.default.readFile(sourcePath),
|
|
298
|
+
sourcePath,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function collectOptionalRootFiles(entries, missingFiles, rootPath, entryPrefix, course) {
|
|
304
|
+
const names = [...ROOT_DIAGNOSTIC_FILE_NAMES];
|
|
305
|
+
if (course !== undefined) {
|
|
306
|
+
names.push(`course-${String(course).padStart(3, '0')}.jsonl`);
|
|
307
|
+
}
|
|
308
|
+
for (const name of names) {
|
|
309
|
+
const entryName = toZipEntryName(entryPrefix, name);
|
|
310
|
+
await collectFileIfPresent(entries, missingFiles, path.join(rootPath, name), entryName);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function filenameContainsDialogParts(filename, rootId, selfId) {
|
|
314
|
+
const parts = [...rootId.split('/'), ...selfId.split('/')];
|
|
315
|
+
return parts.every((part) => filename.includes(part));
|
|
316
|
+
}
|
|
317
|
+
async function collectDebugFiles(entries, missingFiles, dialogsRoot, rootId, selfId) {
|
|
318
|
+
const debugPath = path.join(dialogsRoot, 'debug');
|
|
319
|
+
let dirents;
|
|
320
|
+
try {
|
|
321
|
+
dirents = await promises_1.default.readdir(debugPath, { withFileTypes: true });
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (isNodeFileNotFound(error)) {
|
|
325
|
+
missingFiles.push({ entryName: 'debug/', sourcePath: debugPath });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
dirents.sort((left, right) => left.name.localeCompare(right.name));
|
|
331
|
+
for (const dirent of dirents) {
|
|
332
|
+
if (!dirent.isFile() || !filenameContainsDialogParts(dirent.name, rootId, selfId))
|
|
333
|
+
continue;
|
|
334
|
+
const sourcePath = path.join(debugPath, dirent.name);
|
|
335
|
+
entries.push({
|
|
336
|
+
name: toZipEntryName('debug', dirent.name),
|
|
337
|
+
data: await promises_1.default.readFile(sourcePath),
|
|
338
|
+
sourcePath,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function collectPickedFiles(entries, missingFiles, resolved, request) {
|
|
343
|
+
for (const entryName of request.files) {
|
|
344
|
+
await collectFileIfPresent(entries, missingFiles, sourcePathForBundleEntry(resolved, entryName), entryName);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function assertUniqueEntries(entries) {
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
if (seen.has(entry.name)) {
|
|
351
|
+
throw new Error(`duplicate forensics zip entry: ${entry.name}`);
|
|
352
|
+
}
|
|
353
|
+
seen.add(entry.name);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function buildManifest(params) {
|
|
357
|
+
return {
|
|
358
|
+
generatedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
|
|
359
|
+
rtwsRoot: process.cwd(),
|
|
360
|
+
request: {
|
|
361
|
+
rootId: params.request.rootId,
|
|
362
|
+
selfId: params.request.selfId,
|
|
363
|
+
course: params.request.course ?? null,
|
|
364
|
+
status: params.request.requestedStatus ?? null,
|
|
365
|
+
mode: params.request.mode,
|
|
366
|
+
files: params.request.files,
|
|
367
|
+
},
|
|
368
|
+
resolved: {
|
|
369
|
+
status: params.resolved.status,
|
|
370
|
+
dialogsRoot: params.resolved.dialogsRoot,
|
|
371
|
+
rootPath: params.resolved.rootPath,
|
|
372
|
+
targetPath: params.resolved.targetPath,
|
|
373
|
+
targetKind: params.resolved.targetKind,
|
|
374
|
+
},
|
|
375
|
+
files: params.entries.map((entry) => ({
|
|
376
|
+
entryName: entry.name,
|
|
377
|
+
sourcePath: entry.sourcePath,
|
|
378
|
+
size: entry.data.byteLength,
|
|
379
|
+
})),
|
|
380
|
+
missingFiles: params.missingFiles,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async function collectDialogForensicsZip(request) {
|
|
384
|
+
const resolved = await resolveForensicsPaths(request);
|
|
385
|
+
const entries = [];
|
|
386
|
+
const missingFiles = [];
|
|
387
|
+
if (request.mode === 'pick') {
|
|
388
|
+
await collectPickedFiles(entries, missingFiles, resolved, request);
|
|
389
|
+
}
|
|
390
|
+
else if (resolved.targetKind === 'side') {
|
|
391
|
+
await collectTree(entries, resolved.targetPath, 'side');
|
|
392
|
+
await collectOptionalRootFiles(entries, missingFiles, resolved.rootPath, 'root', request.course);
|
|
393
|
+
await collectDebugFiles(entries, missingFiles, resolved.dialogsRoot, request.rootId, request.selfId);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
await collectOptionalRootFiles(entries, missingFiles, resolved.rootPath, 'dialog', request.course);
|
|
397
|
+
await collectDebugFiles(entries, missingFiles, resolved.dialogsRoot, request.rootId, request.selfId);
|
|
398
|
+
}
|
|
399
|
+
assertUniqueEntries(entries);
|
|
400
|
+
const manifest = buildManifest({ request, resolved, entries, missingFiles });
|
|
401
|
+
const zipEntries = [
|
|
402
|
+
{
|
|
403
|
+
name: 'manifest.json',
|
|
404
|
+
data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, 'utf8'),
|
|
405
|
+
},
|
|
406
|
+
...entries.map((entry) => ({ name: entry.name, data: entry.data })),
|
|
407
|
+
];
|
|
408
|
+
return buildStoreZip(zipEntries);
|
|
409
|
+
}
|
|
410
|
+
function crc32(buffer) {
|
|
411
|
+
let crc = 0xffffffff;
|
|
412
|
+
for (const byte of buffer) {
|
|
413
|
+
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
414
|
+
}
|
|
415
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
416
|
+
}
|
|
417
|
+
function toDosDateTime(date) {
|
|
418
|
+
const year = Math.max(1980, date.getFullYear());
|
|
419
|
+
return {
|
|
420
|
+
time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
|
|
421
|
+
date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function writeUInt32(value) {
|
|
425
|
+
const buffer = Buffer.alloc(4);
|
|
426
|
+
buffer.writeUInt32LE(value >>> 0, 0);
|
|
427
|
+
return buffer;
|
|
428
|
+
}
|
|
429
|
+
function writeUInt16(value) {
|
|
430
|
+
const buffer = Buffer.alloc(2);
|
|
431
|
+
buffer.writeUInt16LE(value, 0);
|
|
432
|
+
return buffer;
|
|
433
|
+
}
|
|
434
|
+
function buildStoreZip(entries) {
|
|
435
|
+
if (entries.length > 0xffff) {
|
|
436
|
+
throw new Error('forensics zip has too many entries for non-ZIP64 output');
|
|
437
|
+
}
|
|
438
|
+
const now = toDosDateTime(new Date());
|
|
439
|
+
const generalPurposeUtf8Flag = 0x0800;
|
|
440
|
+
const localParts = [];
|
|
441
|
+
const centralParts = [];
|
|
442
|
+
let offset = 0;
|
|
443
|
+
for (const entry of entries) {
|
|
444
|
+
const nameBuffer = Buffer.from(entry.name, 'utf8');
|
|
445
|
+
const checksum = crc32(entry.data);
|
|
446
|
+
if (nameBuffer.byteLength > 0xffff) {
|
|
447
|
+
throw new Error(`zip entry name is too long: ${entry.name}`);
|
|
448
|
+
}
|
|
449
|
+
if (entry.data.byteLength > 0xffffffff || offset > 0xffffffff) {
|
|
450
|
+
throw new Error('forensics zip is too large for non-ZIP64 output');
|
|
451
|
+
}
|
|
452
|
+
const localHeader = Buffer.concat([
|
|
453
|
+
writeUInt32(0x04034b50),
|
|
454
|
+
writeUInt16(20),
|
|
455
|
+
writeUInt16(generalPurposeUtf8Flag),
|
|
456
|
+
writeUInt16(0),
|
|
457
|
+
writeUInt16(now.time),
|
|
458
|
+
writeUInt16(now.date),
|
|
459
|
+
writeUInt32(checksum),
|
|
460
|
+
writeUInt32(entry.data.byteLength),
|
|
461
|
+
writeUInt32(entry.data.byteLength),
|
|
462
|
+
writeUInt16(nameBuffer.byteLength),
|
|
463
|
+
writeUInt16(0),
|
|
464
|
+
nameBuffer,
|
|
465
|
+
]);
|
|
466
|
+
localParts.push(localHeader, entry.data);
|
|
467
|
+
const centralHeader = Buffer.concat([
|
|
468
|
+
writeUInt32(0x02014b50),
|
|
469
|
+
writeUInt16(20),
|
|
470
|
+
writeUInt16(20),
|
|
471
|
+
writeUInt16(generalPurposeUtf8Flag),
|
|
472
|
+
writeUInt16(0),
|
|
473
|
+
writeUInt16(now.time),
|
|
474
|
+
writeUInt16(now.date),
|
|
475
|
+
writeUInt32(checksum),
|
|
476
|
+
writeUInt32(entry.data.byteLength),
|
|
477
|
+
writeUInt32(entry.data.byteLength),
|
|
478
|
+
writeUInt16(nameBuffer.byteLength),
|
|
479
|
+
writeUInt16(0),
|
|
480
|
+
writeUInt16(0),
|
|
481
|
+
writeUInt16(0),
|
|
482
|
+
writeUInt16(0),
|
|
483
|
+
writeUInt32(0),
|
|
484
|
+
writeUInt32(offset),
|
|
485
|
+
nameBuffer,
|
|
486
|
+
]);
|
|
487
|
+
centralParts.push(centralHeader);
|
|
488
|
+
offset += localHeader.byteLength + entry.data.byteLength;
|
|
489
|
+
if (offset > 0xffffffff) {
|
|
490
|
+
throw new Error('forensics zip is too large for non-ZIP64 output');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
494
|
+
if (centralDirectory.byteLength > 0xffffffff) {
|
|
495
|
+
throw new Error('forensics zip central directory is too large for non-ZIP64 output');
|
|
496
|
+
}
|
|
497
|
+
const endOfCentralDirectory = Buffer.concat([
|
|
498
|
+
writeUInt32(0x06054b50),
|
|
499
|
+
writeUInt16(0),
|
|
500
|
+
writeUInt16(0),
|
|
501
|
+
writeUInt16(entries.length),
|
|
502
|
+
writeUInt16(entries.length),
|
|
503
|
+
writeUInt32(centralDirectory.byteLength),
|
|
504
|
+
writeUInt32(offset),
|
|
505
|
+
writeUInt16(0),
|
|
506
|
+
]);
|
|
507
|
+
return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory]);
|
|
508
|
+
}
|
|
509
|
+
function forensicsZipFilename(request) {
|
|
510
|
+
const safe = `${request.rootId}-${request.selfId}`.split('/').join('-');
|
|
511
|
+
return `dominds-dialog-forensics-${safe}.zip`;
|
|
512
|
+
}
|
|
513
|
+
async function handleDialogForensicsZipRoute(reqUrl, res) {
|
|
514
|
+
const requestUrl = new URL(reqUrl ?? '/', 'http://127.0.0.1');
|
|
515
|
+
let request;
|
|
516
|
+
try {
|
|
517
|
+
request = parseForensicsRequest(requestUrl);
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
respondJson(res, 400, {
|
|
521
|
+
success: false,
|
|
522
|
+
error: error instanceof Error ? error.message : String(error),
|
|
523
|
+
});
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const zipBuffer = await collectDialogForensicsZip(request);
|
|
528
|
+
res.writeHead(200, {
|
|
529
|
+
'Content-Type': 'application/zip',
|
|
530
|
+
'Content-Disposition': `attachment; filename="${forensicsZipFilename(request)}"`,
|
|
531
|
+
'Cache-Control': 'no-store',
|
|
532
|
+
'X-Content-Type-Options': 'nosniff',
|
|
533
|
+
});
|
|
534
|
+
res.end(zipBuffer);
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
539
|
+
respondJson(res, error instanceof BadForensicsRequestError
|
|
540
|
+
? 400
|
|
541
|
+
: error instanceof ForensicsNotFoundError
|
|
542
|
+
? 404
|
|
543
|
+
: 500, {
|
|
544
|
+
success: false,
|
|
545
|
+
error: message,
|
|
546
|
+
});
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
@@ -892,50 +892,73 @@ function spawnDetachedRestartHelper(params) {
|
|
|
892
892
|
' socket.setTimeout(1000, () => finish(true));',
|
|
893
893
|
' });',
|
|
894
894
|
'}',
|
|
895
|
-
'
|
|
895
|
+
'function getErrorCode(error) {',
|
|
896
|
+
' return error && typeof error === "object" && typeof error.code === "string" ? error.code : "";',
|
|
897
|
+
'}',
|
|
898
|
+
'function assertValidRetiringPid() {',
|
|
899
|
+
' if (!Number.isInteger(payload.retiringPid) || payload.retiringPid <= 0) {',
|
|
900
|
+
' throw new Error(`Invalid retiring Dominds pid for restart: ${String(payload.retiringPid)}`);',
|
|
901
|
+
' }',
|
|
902
|
+
' if (payload.retiringPid === process.pid) {',
|
|
903
|
+
' throw new Error(`Refusing to kill restart helper pid ${String(process.pid)}`);',
|
|
904
|
+
' }',
|
|
905
|
+
'}',
|
|
906
|
+
'function isRetiringProcessAlive() {',
|
|
907
|
+
' assertValidRetiringPid();',
|
|
908
|
+
' try {',
|
|
909
|
+
' process.kill(payload.retiringPid, 0);',
|
|
910
|
+
' return true;',
|
|
911
|
+
' } catch (error) {',
|
|
912
|
+
' const code = getErrorCode(error);',
|
|
913
|
+
' if (code === "ESRCH") return false;',
|
|
914
|
+
' if (code === "EPERM") return true;',
|
|
915
|
+
' throw error;',
|
|
916
|
+
' }',
|
|
917
|
+
'}',
|
|
918
|
+
'async function waitForRestartReadiness(timeoutMs) {',
|
|
896
919
|
' const deadline = Date.now() + timeoutMs;',
|
|
897
|
-
' let
|
|
920
|
+
' let consecutiveReady = 0;',
|
|
898
921
|
' while (Date.now() < deadline) {',
|
|
899
|
-
'
|
|
900
|
-
'
|
|
901
|
-
'
|
|
902
|
-
'
|
|
922
|
+
' const processAlive = isRetiringProcessAlive();',
|
|
923
|
+
' const portBusy = await isPortBusy();',
|
|
924
|
+
' if (!processAlive && !portBusy) {',
|
|
925
|
+
' consecutiveReady += 1;',
|
|
926
|
+
' if (consecutiveReady >= 2) return true;',
|
|
927
|
+
' } else {',
|
|
928
|
+
' consecutiveReady = 0;',
|
|
903
929
|
' }',
|
|
904
|
-
' consecutiveIdle += 1;',
|
|
905
|
-
' if (consecutiveIdle >= 2) return true;',
|
|
906
930
|
' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
|
|
907
931
|
' }',
|
|
908
932
|
' return false;',
|
|
909
933
|
'}',
|
|
910
|
-
'function
|
|
911
|
-
'
|
|
912
|
-
'
|
|
934
|
+
'function runBestEffortKiller(command, args) {',
|
|
935
|
+
' return new Promise((resolve) => {',
|
|
936
|
+
' const killer = spawn(command, args, { stdio: payload.stdioMode });',
|
|
937
|
+
" killer.once('error', () => resolve());",
|
|
938
|
+
" killer.once('exit', () => resolve());",
|
|
939
|
+
' });',
|
|
940
|
+
'}',
|
|
941
|
+
'async function forceKillRetiringProcess() {',
|
|
942
|
+
' assertValidRetiringPid();',
|
|
943
|
+
' try {',
|
|
944
|
+
" process.kill(payload.retiringPid, 'SIGKILL');",
|
|
945
|
+
' } catch (error) {',
|
|
946
|
+
' const code = getErrorCode(error);',
|
|
947
|
+
' if (code === "ESRCH") return;',
|
|
948
|
+
' if (process.platform !== "win32") throw error;',
|
|
913
949
|
' }',
|
|
914
|
-
' if (
|
|
915
|
-
|
|
950
|
+
' if (process.platform === "win32") {',
|
|
951
|
+
" await runBestEffortKiller('taskkill.exe', ['/PID', String(payload.retiringPid), '/F']);",
|
|
916
952
|
' }',
|
|
917
|
-
" const killer = process.platform === 'win32'",
|
|
918
|
-
" ? spawn('taskkill.exe', ['/PID', String(payload.retiringPid), '/F'], { stdio: payload.stdioMode })",
|
|
919
|
-
" : spawn('kill', ['-KILL', String(payload.retiringPid)], { stdio: payload.stdioMode });",
|
|
920
|
-
' return new Promise((resolve, reject) => {',
|
|
921
|
-
" killer.once('error', reject);",
|
|
922
|
-
" killer.once('exit', (code) => {",
|
|
923
|
-
' if (code === 0) {',
|
|
924
|
-
' resolve();',
|
|
925
|
-
' return;',
|
|
926
|
-
' }',
|
|
927
|
-
' resolve();',
|
|
928
|
-
' });',
|
|
929
|
-
' });',
|
|
930
953
|
'}',
|
|
931
954
|
'(async () => {',
|
|
932
955
|
' try {',
|
|
933
|
-
' const
|
|
934
|
-
' if (!
|
|
956
|
+
' const readyGracefully = await waitForRestartReadiness(payload.forceKillAfterMs);',
|
|
957
|
+
' if (!readyGracefully) {',
|
|
935
958
|
' await forceKillRetiringProcess();',
|
|
936
|
-
' const
|
|
937
|
-
' if (!
|
|
938
|
-
' throw new Error(`Dominds restart
|
|
959
|
+
' const readyAfterKill = await waitForRestartReadiness(payload.portReleaseTimeoutMs);',
|
|
960
|
+
' if (!readyAfterKill) {',
|
|
961
|
+
' throw new Error(`Dominds restart target is still not ready after force-killing pid ${String(payload.retiringPid)}; port=${String(payload.host)}:${String(payload.port)}`);',
|
|
939
962
|
' }',
|
|
940
963
|
' }',
|
|
941
964
|
" const child = spawn(payload.command, payload.args, { cwd: payload.cwd, env: process.env, detached, stdio: payload.stdioMode, shell: process.platform === 'win32' });",
|