querysub 0.570.0 → 0.571.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/appSecrets.ts +1 -0
- package/package.json +1 -1
- package/src/deployManager/components/ProcessesView.tsx +5 -4
- package/src/deployManager/machineApplyMainCode.ts +41 -39
- package/src/deployManager/machineController.ts +15 -9
- package/src/deployManager/processLogs.ts +32 -41
- package/src/deployManager/processManager.ts +99 -97
package/appSecrets.ts
CHANGED
package/package.json
CHANGED
|
@@ -32,14 +32,15 @@ class ProcessOutput extends qreact.Component<{ record: ProcessRecord; machineNod
|
|
|
32
32
|
private unmounted = false;
|
|
33
33
|
componentDidMount() {
|
|
34
34
|
// Props are synchronized state, so the plain values the async work needs are read here
|
|
35
|
-
let { folder,
|
|
35
|
+
let { folder, pid, startTime } = this.props.record;
|
|
36
36
|
let nodeId = this.props.machineNodeId;
|
|
37
37
|
let callbackId = this.callbackId;
|
|
38
38
|
this.watching = (async () => {
|
|
39
39
|
await watchProcessOutput({
|
|
40
40
|
nodeId,
|
|
41
41
|
folder,
|
|
42
|
-
|
|
42
|
+
pid,
|
|
43
|
+
startTime,
|
|
43
44
|
callbackId,
|
|
44
45
|
onData: async (data: string) => {
|
|
45
46
|
Querysub.commit(() => {
|
|
@@ -153,9 +154,9 @@ class MachineProcesses extends qreact.Component<{ machineId: string; applyNodeId
|
|
|
153
154
|
{this.state.showHistorical && `Hide ${historical.length} historical` || `Show ${historical.length} historical`}
|
|
154
155
|
</Button>}
|
|
155
156
|
</div>
|
|
156
|
-
{running.map(record => <ProcessRow key={record.
|
|
157
|
+
{running.map(record => <ProcessRow key={`${record.pid}-${record.startTime}`} record={record} machineNodeId={this.props.applyNodeId} />)}
|
|
157
158
|
{this.state.showHistorical && historical.map(record =>
|
|
158
|
-
<ProcessRow key={record.
|
|
159
|
+
<ProcessRow key={`${record.pid}-${record.startTime}`} record={record} machineNodeId={this.props.applyNodeId} />
|
|
159
160
|
)}
|
|
160
161
|
</div>;
|
|
161
162
|
}
|
|
@@ -26,8 +26,8 @@ import { PromiseObj } from "../promise";
|
|
|
26
26
|
import path from "path";
|
|
27
27
|
import { fsExistsAsync } from "../fs";
|
|
28
28
|
import { ALIVE_WINDOW_FOREVER, ParametersTimelineEntry, syncParametersTimelineFiles } from "./parametersTimeline";
|
|
29
|
-
import { getScreenName, getFutureScreenName,
|
|
30
|
-
import { ProcessRecord, syncProcessRecords,
|
|
29
|
+
import { getScreenName, getFutureScreenName, isRetiring, getTmuxPrefix, getScreenState, isScreenRunningProcess, getProcessStartTime, getScreenPanePid, ensureProcessRecord, runScreenCommand, killScreen, readServiceNodeId, removeServiceNode, takeoverScreen, scheduleTakeover, withScreenLock } from "./processManager";
|
|
30
|
+
import { ProcessRecord, syncProcessRecords, listProcessRecords } from "./processLogs";
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
@@ -298,10 +298,9 @@ function getDesiredScreens(configs: ServiceConfig[], machineId: string, root: st
|
|
|
298
298
|
/** Puts one screen into the state its configuration describes: the folder synced, the process running what it should be, and the records and timeline saying so. Everything about one screen happens under its lock, so a resync and a scheduled takeover can never interleave. */
|
|
299
299
|
async function ensureScreen(config: {
|
|
300
300
|
desired: DesiredScreen;
|
|
301
|
-
screenStateMap: Map<string, { screenName: string; isProcessRunning: boolean; pid: string }>;
|
|
302
301
|
machineInfo: MachineInfo;
|
|
303
302
|
}): Promise<void> {
|
|
304
|
-
let { desired,
|
|
303
|
+
let { desired, machineInfo } = config;
|
|
305
304
|
let { screenName, canonicalScreenName, folder, parameters, serviceId } = desired;
|
|
306
305
|
|
|
307
306
|
let launchCount = launchesPerService.get(serviceId) || 0;
|
|
@@ -315,15 +314,16 @@ async function ensureScreen(config: {
|
|
|
315
314
|
|
|
316
315
|
try {
|
|
317
316
|
await withScreenLock(canonicalScreenName, async () => {
|
|
318
|
-
|
|
317
|
+
// Asked of tmux here, not handed in: this decides everything from the names it was given
|
|
318
|
+
let panePid = await getScreenPanePid(screenName);
|
|
319
|
+
let isUp = !!panePid && await isScreenRunningProcess(panePid);
|
|
319
320
|
|
|
320
321
|
if (desired.outgoing) {
|
|
321
322
|
// Its replacement owns the folder now, so the only thing to do is keep its record straight and let it know when it goes down
|
|
322
|
-
if (
|
|
323
|
-
await ensureProcessRecord({ screenName, folder, panePid
|
|
323
|
+
if (panePid && isUp) {
|
|
324
|
+
await ensureProcessRecord({ screenName, folder, panePid, record: desired });
|
|
324
325
|
void (async () => {
|
|
325
|
-
let
|
|
326
|
-
let nodeId = running?.nodeId || await readServiceNodeId(folder);
|
|
326
|
+
let nodeId = await readServiceNodeId(folder);
|
|
327
327
|
if (nodeId) {
|
|
328
328
|
await notifyNodeShutdown(screenName, nodeId, desired.aliveWindow[1]);
|
|
329
329
|
}
|
|
@@ -361,54 +361,61 @@ async function ensureScreen(config: {
|
|
|
361
361
|
machineInfo.services[serviceId].nodeId = await fs.promises.readFile(nodePathId, "utf8");
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
if (sameRestartParameters(prevParameters, parameters)
|
|
364
|
+
if (isUp && panePid && sameRestartParameters(prevParameters, parameters)) {
|
|
365
365
|
if (prevParameters !== newParametersString) {
|
|
366
366
|
await fs.promises.writeFile(parameterPath, newParametersString);
|
|
367
367
|
}
|
|
368
368
|
// We may not have launched this one (we restarted, or it predates process records), so make sure it is recorded before leaving it alone
|
|
369
|
-
await ensureProcessRecord({ screenName, folder, panePid
|
|
370
|
-
await syncParametersTimelineFiles(folder, [{ pid: parseInt(
|
|
369
|
+
await ensureProcessRecord({ screenName, folder, panePid, record: desired });
|
|
370
|
+
await syncParametersTimelineFiles(folder, [{ pid: parseInt(panePid) || undefined, aliveWindow: desired.aliveWindow, parameters }]);
|
|
371
|
+
// A restart loses the timer that retires the version this one replaces, so it is put back every time we see it still waiting
|
|
372
|
+
if (desired.takeoverAt !== undefined) {
|
|
373
|
+
scheduleTakeover({ canonicalScreenName, folder, killTime: desired.takeoverAt });
|
|
374
|
+
}
|
|
371
375
|
console.log(green(`Verified ${magenta(screenName)} is running`));
|
|
372
376
|
return;
|
|
373
377
|
}
|
|
374
378
|
|
|
375
|
-
console.log(`Resyncing ${magenta(screenName)}, with ${newParametersString},
|
|
379
|
+
console.log(`Resyncing ${magenta(screenName)}, with ${newParametersString}, isUp = ${isUp}`);
|
|
376
380
|
await fs.promises.writeFile(parameterPath, newParametersString);
|
|
377
381
|
void recordLaunch({
|
|
378
382
|
serviceId,
|
|
379
383
|
serviceKey: desired.serviceKey,
|
|
380
384
|
screenName: canonicalScreenName,
|
|
381
385
|
machineId: desired.machineId,
|
|
382
|
-
reason:
|
|
386
|
+
reason: isUp && "update" || "crashed",
|
|
383
387
|
time: Date.now(),
|
|
384
388
|
});
|
|
385
389
|
|
|
390
|
+
// The node the version being replaced registered as, read before the launch overwrites the file with the new process's
|
|
391
|
+
let replacedNodeId = await readServiceNodeId(folder);
|
|
392
|
+
|
|
386
393
|
// Every launch goes into the future name and renames over the canonical one, releases and plain restarts alike: the replacement is up before anything touches the old process, and it always gets a brand new pane - which is what makes it a distinct process with its own log.
|
|
387
394
|
let launchScreenName = getFutureScreenName(canonicalScreenName);
|
|
388
|
-
let
|
|
395
|
+
let launchedPid = await runScreenCommand({
|
|
389
396
|
screenName: launchScreenName,
|
|
390
397
|
folder,
|
|
391
398
|
command: parameters.command,
|
|
392
399
|
record: desired,
|
|
393
|
-
// Scheduled the instant the command is sent. Without a time the old process goes as soon as this one is verified.
|
|
394
|
-
takeover: desired.takeoverAt !== undefined && { canonicalScreenName, killTime: desired.takeoverAt } || undefined,
|
|
395
400
|
});
|
|
401
|
+
// The command is running as of the line above, so the overlap is measured from here
|
|
402
|
+
if (desired.takeoverAt !== undefined) {
|
|
403
|
+
scheduleTakeover({ canonicalScreenName, folder, killTime: desired.takeoverAt, replacedNodeId });
|
|
404
|
+
}
|
|
396
405
|
|
|
397
406
|
await delay(LAUNCH_VERIFY_DELAY);
|
|
398
|
-
let
|
|
399
|
-
|
|
400
|
-
if (!launched || !isRunning) {
|
|
407
|
+
let launchedPanePid = await getScreenPanePid(launchScreenName);
|
|
408
|
+
if (!launchedPanePid || !await isScreenRunningProcess(launchedPanePid)) {
|
|
401
409
|
let prefix = getTmuxPrefix();
|
|
402
410
|
let logs = await runPromise(`${prefix}tmux capture-pane -t ${launchScreenName} -p`);
|
|
403
411
|
throw new Error(`${logs}\n\nService ${magenta(launchScreenName)} is not running after starting. Trying again in ${formatTime(MACHINE_RESYNC_INTERVAL)}, or on next change. Last logs above.`);
|
|
404
412
|
}
|
|
405
|
-
console.log(green(`${magenta(launchScreenName)} is verified to be running (
|
|
413
|
+
console.log(green(`${magenta(launchScreenName)} is verified to be running (pid ${launchedPid})`));
|
|
406
414
|
if (desired.takeoverAt === undefined) {
|
|
407
415
|
// Nothing is holding the canonical name open for an overlap, so take it now
|
|
408
|
-
await takeoverScreen({ canonicalScreenName, folder });
|
|
409
|
-
screenStateMap.set(canonicalScreenName, { ...launched, screenName: canonicalScreenName });
|
|
416
|
+
await takeoverScreen({ canonicalScreenName, folder, replacedNodeId });
|
|
410
417
|
}
|
|
411
|
-
await syncParametersTimelineFiles(folder, [{ pid: parseInt(
|
|
418
|
+
await syncParametersTimelineFiles(folder, [{ pid: parseInt(launchedPanePid) || undefined, aliveWindow: desired.aliveWindow, parameters }]);
|
|
412
419
|
|
|
413
420
|
launchCount++;
|
|
414
421
|
lastLaunchedTime = Date.now();
|
|
@@ -445,30 +452,25 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
445
452
|
|
|
446
453
|
let machineInfo = await getLiveMachineInfo();
|
|
447
454
|
|
|
455
|
+
// The whole machine's screens, which is what the record maintenance and the kill sweep are about. Converging a screen asks about that one screen itself.
|
|
448
456
|
let screenState = await getScreenState();
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
let panePids = new Set(screenState.map(x => x.pid));
|
|
457
|
+
// A process is alive while its pane still exists, still reports the start time it was named after (a reused pid is a different process), and still has something running in it - a pane whose command exited is a dead process, not a live one
|
|
458
|
+
let running = new Set(screenState.filter(x => x.isProcessRunning).map(x => x.pid));
|
|
452
459
|
await syncProcessRecords({
|
|
453
460
|
isAlive: async record => {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return await getProcessStartTime(pid) === record.startTime;
|
|
461
|
+
if (!running.has(record.pid)) return false;
|
|
462
|
+
return await getProcessStartTime(record.pid) === record.startTime;
|
|
457
463
|
},
|
|
458
464
|
now: Date.now(),
|
|
459
465
|
});
|
|
460
466
|
let root = os.homedir() + "/" + SERVICE_FOLDER;
|
|
461
467
|
let desiredScreens = getDesiredScreens(relevantConfigs, machineId, root);
|
|
462
468
|
|
|
463
|
-
//
|
|
469
|
+
// The fast-path results are memoized per resync, so a folder two desired screens share is only traversed once
|
|
464
470
|
gitFastPathChecks.clear();
|
|
465
|
-
for (let desired of desiredScreens) {
|
|
466
|
-
if (desired.outgoing || !desired.parameters.gitRef) continue;
|
|
467
|
-
void checkGitSyncedAndClean(desired.folder + "git/", desired.parameters.gitRef);
|
|
468
|
-
}
|
|
469
471
|
|
|
470
472
|
// Every screen converges concurrently. Each has its own folder, its own records and its own lock, and yarn install takes a network mutex, so there is nothing to serialize behind - one slow git sync or shutdown no longer delays everything after it.
|
|
471
|
-
await Promise.all(desiredScreens.map(desired => ensureScreen({ desired,
|
|
473
|
+
await Promise.all(desiredScreens.map(desired => ensureScreen({ desired, machineInfo })));
|
|
472
474
|
|
|
473
475
|
// Both names count as wanted: a screen that took over during this pass answers to the canonical one now, and one that is about to take over still answers to its future name
|
|
474
476
|
let wantedNames = new Set<string>();
|
|
@@ -478,13 +480,13 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
478
480
|
}
|
|
479
481
|
for (let { screenName } of screenState) {
|
|
480
482
|
if (wantedNames.has(screenName)) continue;
|
|
481
|
-
//
|
|
482
|
-
if (screenName
|
|
483
|
+
// Already being taken down. One left over from a restart is not, so it still gets cleaned up here.
|
|
484
|
+
if (isRetiring(screenName)) continue;
|
|
483
485
|
// Detached, so a screen that takes its full grace period to go down doesn't hold up the end of the resync
|
|
484
486
|
void (async () => {
|
|
485
487
|
// Nothing is replacing this one, so its registration and the file it wrote both go
|
|
486
488
|
let folder = root + screenName + "/";
|
|
487
|
-
let nodeId =
|
|
489
|
+
let nodeId = await readServiceNodeId(folder);
|
|
488
490
|
if (nodeId) {
|
|
489
491
|
await removeServiceNode({ folder, nodeId, ownsFile: true });
|
|
490
492
|
}
|
|
@@ -83,16 +83,18 @@ class MachineControllerBase {
|
|
|
83
83
|
}
|
|
84
84
|
public async streamProcessOutput(config: {
|
|
85
85
|
folder: string;
|
|
86
|
-
|
|
86
|
+
pid: string;
|
|
87
|
+
startTime: number;
|
|
87
88
|
callbackId: string;
|
|
88
89
|
}): Promise<void> {
|
|
89
90
|
let caller = SocketFunction.getCaller();
|
|
90
91
|
await streamProcessOutput({
|
|
91
92
|
folder: config.folder,
|
|
92
|
-
|
|
93
|
+
pid: config.pid,
|
|
94
|
+
startTime: config.startTime,
|
|
93
95
|
onData: async (data) => {
|
|
94
96
|
await MachineControllerClient.nodes[caller.nodeId].onScreenOutput({
|
|
95
|
-
|
|
97
|
+
pid: config.pid,
|
|
96
98
|
data,
|
|
97
99
|
callbackId: config.callbackId,
|
|
98
100
|
});
|
|
@@ -106,14 +108,16 @@ class MachineControllerBase {
|
|
|
106
108
|
public async watchOtherProcessOutput(config: {
|
|
107
109
|
nodeId: string;
|
|
108
110
|
folder: string;
|
|
109
|
-
|
|
111
|
+
pid: string;
|
|
112
|
+
startTime: number;
|
|
110
113
|
callbackId: string;
|
|
111
114
|
}) {
|
|
112
115
|
let caller = SocketFunction.getCaller();
|
|
113
116
|
forwardedCallbacks.set(config.callbackId, caller.nodeId);
|
|
114
117
|
await MachineController(config.nodeId).streamProcessOutput.promise({
|
|
115
118
|
folder: config.folder,
|
|
116
|
-
|
|
119
|
+
pid: config.pid,
|
|
120
|
+
startTime: config.startTime,
|
|
117
121
|
callbackId: config.callbackId,
|
|
118
122
|
});
|
|
119
123
|
}
|
|
@@ -170,7 +174,8 @@ let callbacks = new Map<string, (data: string) => Promise<void>>();
|
|
|
170
174
|
export async function watchProcessOutput(config: {
|
|
171
175
|
nodeId: string;
|
|
172
176
|
folder: string;
|
|
173
|
-
|
|
177
|
+
pid: string;
|
|
178
|
+
startTime: number;
|
|
174
179
|
callbackId: string;
|
|
175
180
|
onData: (data: string) => Promise<void>;
|
|
176
181
|
}) {
|
|
@@ -179,7 +184,8 @@ export async function watchProcessOutput(config: {
|
|
|
179
184
|
await MachineController(SocketFunction.browserNodeId()).watchOtherProcessOutput.promise({
|
|
180
185
|
nodeId: config.nodeId,
|
|
181
186
|
folder: config.folder,
|
|
182
|
-
|
|
187
|
+
pid: config.pid,
|
|
188
|
+
startTime: config.startTime,
|
|
183
189
|
callbackId,
|
|
184
190
|
});
|
|
185
191
|
}
|
|
@@ -192,14 +198,14 @@ export async function stopWatchingProcessOutput(config: {
|
|
|
192
198
|
}
|
|
193
199
|
class MachineControllerClientBase {
|
|
194
200
|
public async onScreenOutput(config: {
|
|
195
|
-
|
|
201
|
+
pid: string;
|
|
196
202
|
data: string;
|
|
197
203
|
callbackId: string;
|
|
198
204
|
}): Promise<void> {
|
|
199
205
|
let forwardToNodeId = forwardedCallbacks.get(config.callbackId);
|
|
200
206
|
if (forwardToNodeId) {
|
|
201
207
|
await MachineControllerClient.nodes[forwardToNodeId].onScreenOutput({
|
|
202
|
-
|
|
208
|
+
pid: config.pid,
|
|
203
209
|
data: config.data,
|
|
204
210
|
callbackId: config.callbackId,
|
|
205
211
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from "os";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { fsExistsAsync } from "../fs";
|
|
4
5
|
import { SERVICE_FOLDER, ServiceParameters } from "./machineSchema";
|
|
5
6
|
|
|
6
7
|
// One log file per process, appended to until it passes LOG_SIZE_LIMIT, then emptied wholesale. Watchers see the file shrink and re-seek to the start. A watcher attaching in the moment right after a reset sees an empty log, which at this size is rare enough to be worth how much simpler this is than rolling files.
|
|
@@ -16,53 +17,43 @@ const LOG_SUFFIX = ".log";
|
|
|
16
17
|
const PIPE_SCRIPT_SUFFIX = ".pipe.sh";
|
|
17
18
|
const TAIL_SCRIPT_SUFFIX = ".tail.sh";
|
|
18
19
|
|
|
19
|
-
/** One
|
|
20
|
+
/** One process: the configuration it was started with, and its output. A process is named by its pid and the start time the OS reports for that pid - the pid alone gets reused, and both halves can be re-checked against the machine at any time instead of trusted from our own bookkeeping. */
|
|
20
21
|
export type ProcessRecord = {
|
|
21
|
-
|
|
22
|
-
/** The
|
|
22
|
+
pid: string;
|
|
23
|
+
/** The start time the OS reports for the pid */
|
|
24
|
+
startTime: number;
|
|
25
|
+
/** The instance folder this process runs out of. A replacement shares the folder with the process it replaces, so this is what owns the logs, not the tmux session name. */
|
|
23
26
|
folder: string;
|
|
24
|
-
/** The tmux session, which a takeover renames
|
|
27
|
+
/** The tmux session, which a takeover renames */
|
|
25
28
|
screenName: string;
|
|
26
29
|
serviceId: string;
|
|
27
30
|
serviceKey: string;
|
|
28
31
|
/** Which instance of the service this is, on this machine */
|
|
29
32
|
index: number;
|
|
30
33
|
machineId: string;
|
|
31
|
-
/** The
|
|
32
|
-
startTime: number;
|
|
33
|
-
/** The node this process registered as, read out of the folder before any replacement is started - after that the file belongs to the replacement. Kept here so taking this process down needs nothing but its own record. */
|
|
34
|
+
/** The node this process registered as, read out of the folder before any replacement is started - after that the file belongs to the replacement. */
|
|
34
35
|
nodeId?: string;
|
|
35
36
|
/** When we first noticed the process was gone. Absent while it is running. */
|
|
36
37
|
deadTime?: number;
|
|
37
|
-
|
|
38
|
-
/** The parameters it was launched with, with template variables already resolved */
|
|
38
|
+
/** The parameters it was started with, with template variables already resolved */
|
|
39
39
|
parameters: ServiceParameters;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
function getRecordPath(folder: string, launchId: string): string {
|
|
46
|
-
return path.join(folder, PROCESS_FOLDER, launchId + RECORD_SUFFIX);
|
|
42
|
+
// A process's files all share this name, so its whole footprint is found, and deleted, together
|
|
43
|
+
function getProcessFileName(pid: string, startTime: number): string {
|
|
44
|
+
return `${pid}-${startTime}`;
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return path.join(folder, PROCESS_FOLDER, launchId + PIPE_SCRIPT_SUFFIX);
|
|
46
|
+
export function getProcessLogPath(folder: string, pid: string, startTime: number): string {
|
|
47
|
+
return path.join(folder, PROCESS_FOLDER, getProcessFileName(pid, startTime) + LOG_SUFFIX);
|
|
51
48
|
}
|
|
52
|
-
export function
|
|
53
|
-
return path.join(folder, PROCESS_FOLDER,
|
|
49
|
+
export function getPipeScriptPath(folder: string, pid: string, startTime: number): string {
|
|
50
|
+
return path.join(folder, PROCESS_FOLDER, getProcessFileName(pid, startTime) + PIPE_SCRIPT_SUFFIX);
|
|
54
51
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
export function createLaunchId(pid: string, startTime: number): string {
|
|
58
|
-
return `${pid}-${startTime}`;
|
|
52
|
+
export function getTailScriptPath(folder: string, pid: string, startTime: number): string {
|
|
53
|
+
return path.join(folder, PROCESS_FOLDER, getProcessFileName(pid, startTime) + TAIL_SCRIPT_SUFFIX);
|
|
59
54
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
pid: launchId.slice(0, separator),
|
|
64
|
-
startTime: parseInt(launchId.slice(separator + 1)) || 0,
|
|
65
|
-
};
|
|
55
|
+
function getRecordPath(folder: string, pid: string, startTime: number): string {
|
|
56
|
+
return path.join(folder, PROCESS_FOLDER, getProcessFileName(pid, startTime) + RECORD_SUFFIX);
|
|
66
57
|
}
|
|
67
58
|
|
|
68
59
|
/** The shell a screen's output is piped through: appends every line, and empties the file once it grows past the limit. */
|
|
@@ -101,16 +92,16 @@ while true; do
|
|
|
101
92
|
done`;
|
|
102
93
|
}
|
|
103
94
|
|
|
104
|
-
/**
|
|
105
|
-
export async function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
95
|
+
/** One process's record, read straight from the file its pid and start time name. */
|
|
96
|
+
export async function readProcessRecord(folder: string, pid: string, startTime: number): Promise<ProcessRecord | undefined> {
|
|
97
|
+
let file = getRecordPath(folder, pid, startTime);
|
|
98
|
+
if (!await fsExistsAsync(file)) return undefined;
|
|
99
|
+
return JSON.parse(await fs.promises.readFile(file, "utf8")) as ProcessRecord;
|
|
109
100
|
}
|
|
110
101
|
|
|
111
102
|
export async function writeProcessRecord(record: ProcessRecord): Promise<void> {
|
|
112
103
|
await fs.promises.mkdir(path.join(record.folder, PROCESS_FOLDER), { recursive: true });
|
|
113
|
-
await fs.promises.writeFile(getRecordPath(record.folder, record.
|
|
104
|
+
await fs.promises.writeFile(getRecordPath(record.folder, record.pid, record.startTime), JSON.stringify(record));
|
|
114
105
|
}
|
|
115
106
|
|
|
116
107
|
async function readFolderRecords(folder: string): Promise<ProcessRecord[]> {
|
|
@@ -147,10 +138,10 @@ export async function listProcessRecords(): Promise<ProcessRecord[]> {
|
|
|
147
138
|
|
|
148
139
|
async function removeProcessRecord(record: ProcessRecord): Promise<void> {
|
|
149
140
|
let files = [
|
|
150
|
-
getRecordPath(record.folder, record.
|
|
151
|
-
getProcessLogPath(record.folder, record.
|
|
152
|
-
getPipeScriptPath(record.folder, record.
|
|
153
|
-
getTailScriptPath(record.folder, record.
|
|
141
|
+
getRecordPath(record.folder, record.pid, record.startTime),
|
|
142
|
+
getProcessLogPath(record.folder, record.pid, record.startTime),
|
|
143
|
+
getPipeScriptPath(record.folder, record.pid, record.startTime),
|
|
144
|
+
getTailScriptPath(record.folder, record.pid, record.startTime),
|
|
154
145
|
];
|
|
155
146
|
for (let file of files) {
|
|
156
147
|
try {
|
|
@@ -169,11 +160,11 @@ export async function syncProcessRecords(config: {
|
|
|
169
160
|
for (let record of await listProcessRecords()) {
|
|
170
161
|
if (record.deadTime === undefined && !await config.isAlive(record)) {
|
|
171
162
|
record.deadTime = config.now;
|
|
172
|
-
console.log(`Process ${record.
|
|
163
|
+
console.log(`Process ${record.pid} (${record.screenName}) is no longer running, marking it dead. Its log is deleted at ${new Date(record.deadTime + DEAD_PROCESS_RETENTION).toISOString()}`);
|
|
173
164
|
await writeProcessRecord(record);
|
|
174
165
|
}
|
|
175
166
|
if (record.deadTime !== undefined && config.now - record.deadTime > DEAD_PROCESS_RETENTION) {
|
|
176
|
-
console.log(`Deleting the log of process ${record.
|
|
167
|
+
console.log(`Deleting the log of process ${record.pid} (${record.screenName}), dead since ${new Date(record.deadTime).toISOString()}`);
|
|
177
168
|
await removeProcessRecord(record);
|
|
178
169
|
}
|
|
179
170
|
}
|
|
@@ -2,7 +2,7 @@ import os from "os";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { spawn, ChildProcess } from "child_process";
|
|
5
|
-
import { lazy } from "socket-function/src/caching";
|
|
5
|
+
import { cache, lazy } from "socket-function/src/caching";
|
|
6
6
|
import { measureWrap } from "socket-function/src/profiling/measure";
|
|
7
7
|
import { delay, runInSerial } from "socket-function/src/batching";
|
|
8
8
|
import { timeInSecond } from "socket-function/src/misc";
|
|
@@ -13,7 +13,7 @@ import { forceRemoveNode } from "../-f-node-discovery/NodeDiscovery";
|
|
|
13
13
|
import { fsExistsAsync } from "../fs";
|
|
14
14
|
import { PromiseObj } from "../promise";
|
|
15
15
|
import { SERVICE_FOLDER, SERVICE_NODE_FILE_NAME } from "./machineSchema";
|
|
16
|
-
import { ProcessRecord,
|
|
16
|
+
import { ProcessRecord, getLogPipeScript, getLogTailScript, getProcessLogPath, getPipeScriptPath, getTailScriptPath, readProcessRecord, writeProcessRecord } from "./processLogs";
|
|
17
17
|
import { setPreciseTimeout } from "../misc";
|
|
18
18
|
|
|
19
19
|
// Running, inspecting and killing the tmux screens services run in. This layer only knows "here is a configuration, run it" - which version should be running when is the deploy logic's problem.
|
|
@@ -38,25 +38,29 @@ export function getFutureScreenName(canonicalScreenName: string): string {
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
// Everything that touches a service's screens - the resync loop, and the timer that retires the old process at its kill time - runs under this, keyed by the canonical screen name. They fire at similar times by design, and a takeover is a rename dance that must not interleave with another one.
|
|
41
|
-
const
|
|
42
|
-
export function withScreenLock<T>(canonicalScreenName: string, run: () => Promise<T>): Promise<T> {
|
|
43
|
-
|
|
44
|
-
let next = previous.then(run, run);
|
|
45
|
-
// Failures release the lock rather than poisoning every later operation on this screen
|
|
46
|
-
screenLocks.set(canonicalScreenName, next.then(() => { }, () => { }));
|
|
47
|
-
return next;
|
|
41
|
+
const getScreenLock = cache((canonicalScreenName: string) => runInSerial(async (run: () => Promise<unknown>) => await run()));
|
|
42
|
+
export async function withScreenLock<T>(canonicalScreenName: string, run: () => Promise<T>): Promise<T> {
|
|
43
|
+
return await getScreenLock(canonicalScreenName)(run) as T;
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
/**
|
|
51
|
-
async function
|
|
46
|
+
/** The pid of a screen's pane, or undefined when there is no such session. Asks about the one screen instead of listing every session on the machine. */
|
|
47
|
+
export async function getScreenPanePid(screenName: string): Promise<string | undefined> {
|
|
52
48
|
let prefix = getTmuxPrefix();
|
|
49
|
+
let output: string;
|
|
53
50
|
try {
|
|
54
|
-
await runPromise(`${prefix}tmux
|
|
55
|
-
return true;
|
|
51
|
+
output = await runPromise(`${prefix}tmux list-panes -t ${screenName} -F "#{pane_pid}"`, { quiet: true });
|
|
56
52
|
} catch {
|
|
57
|
-
//
|
|
58
|
-
return
|
|
53
|
+
// Exits non-zero for "no such session", which is an answer, not a failure
|
|
54
|
+
return undefined;
|
|
59
55
|
}
|
|
56
|
+
return output.trim().split("\n")[0].trim() || undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether a screen exists AND has a process running in it - the only sense in which a service is up. */
|
|
60
|
+
export async function isScreenUp(screenName: string): Promise<boolean> {
|
|
61
|
+
let panePid = await getScreenPanePid(screenName);
|
|
62
|
+
if (!panePid) return false;
|
|
63
|
+
return await isScreenRunningProcess(panePid);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/** The node the process in this folder registered as. Read it BEFORE starting a replacement in the same folder - once the replacement starts, the file is the replacement's. */
|
|
@@ -215,104 +219,76 @@ export const runScreenCommand = measureWrap(async function runScreenCommand(conf
|
|
|
215
219
|
command: string;
|
|
216
220
|
// Defaults to the folder derived from screenName; a future screen passes its canonical screen's folder
|
|
217
221
|
folder?: string;
|
|
218
|
-
// Identifies the process this
|
|
219
|
-
record: Omit<ProcessRecord, "
|
|
220
|
-
// The screen this launch replaces, and when that one's overlap runs out. The timer is set the instant the command is sent, so the overlap is measured from the new process actually starting.
|
|
221
|
-
takeover?: { canonicalScreenName: string; killTime: number };
|
|
222
|
+
// Identifies the process this start creates, so its log and its configuration are stored against it
|
|
223
|
+
record: Omit<ProcessRecord, "pid" | "startTime" | "folder" | "screenName">;
|
|
222
224
|
}): Promise<string> {
|
|
223
225
|
let prefix = getTmuxPrefix();
|
|
224
226
|
let screenName = config.screenName;
|
|
225
227
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
console.log(red(`Removing the leftover session ${screenName} from a launch that never completed`));
|
|
228
|
+
// Always a brand new session, so the pane is always new - and the pane's pid is what makes this a distinct process with its own log. A session already under this name is debris from a start that never got taken over.
|
|
229
|
+
if (await getScreenPanePid(screenName)) {
|
|
230
|
+
console.log(red(`Removing the leftover session ${screenName} from a start that never completed`));
|
|
230
231
|
await runPromise(`${prefix}tmux kill-session -t ${screenName}`);
|
|
231
232
|
}
|
|
232
233
|
await runPromise(`${prefix}tmux new -s ${screenName} -d`);
|
|
233
234
|
|
|
234
235
|
let folder = config.folder || os.homedir() + "/" + SERVICE_FOLDER + screenName + "/";
|
|
235
236
|
|
|
236
|
-
//
|
|
237
|
-
let
|
|
238
|
-
if (
|
|
239
|
-
for (let record of await listProcessRecords()) {
|
|
240
|
-
if (record.folder !== folder || record.deadTime !== undefined || record.nodeId) continue;
|
|
241
|
-
await writeProcessRecord({ ...record, nodeId: outgoingNodeId });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// The pane we are about to run the command in identifies the process: its pid, and the start time the OS reports for that pid. Both can be re-checked against the machine later, so nothing downstream has to trust our bookkeeping about what is still running. The session was just created, so this pid is always new.
|
|
246
|
-
let panePid = (await getScreenState(false)).find(x => x.screenName === screenName)?.pid;
|
|
247
|
-
if (!panePid) {
|
|
237
|
+
// The pane we are about to run the command in names the process: its pid, and the start time the OS reports for that pid. Both can be re-checked against the machine later, so nothing downstream trusts our bookkeeping about what is still running.
|
|
238
|
+
let pid = await getScreenPanePid(screenName);
|
|
239
|
+
if (!pid) {
|
|
248
240
|
throw new Error(`Screen ${screenName} does not exist after creating it, so there is no process to run the command in`);
|
|
249
241
|
}
|
|
250
|
-
let startTime = await getProcessStartTime(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
launchId,
|
|
255
|
-
folder,
|
|
256
|
-
screenName,
|
|
257
|
-
pid: parseInt(panePid) || undefined,
|
|
258
|
-
startTime,
|
|
259
|
-
});
|
|
242
|
+
let startTime = await getProcessStartTime(pid);
|
|
243
|
+
await writeProcessRecord({ ...config.record, pid, startTime, folder, screenName });
|
|
244
|
+
// The pipe is attached before the command runs, so the log holds everything the process ever printed
|
|
245
|
+
await setupPipePane({ screenName, folder, pid, startTime });
|
|
260
246
|
await runPromise(`${prefix}tmux send-keys -t ${screenName} 'cd ${folder}git' Enter`);
|
|
261
247
|
let command = `#!/bin/bash
|
|
262
248
|
${config.command}
|
|
263
249
|
`;
|
|
264
250
|
await fs.promises.writeFile(folder + "command.sh", command);
|
|
251
|
+
// Sending the command is the LAST thing this does, so the caller's next line runs the instant the process starts
|
|
265
252
|
await runPromise(`${prefix}tmux send-keys -t ${screenName} 'bash ../command.sh' Enter`);
|
|
266
|
-
|
|
267
|
-
if (config.takeover) {
|
|
268
|
-
scheduleTakeover({
|
|
269
|
-
canonicalScreenName: config.takeover.canonicalScreenName,
|
|
270
|
-
folder,
|
|
271
|
-
killTime: config.takeover.killTime,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
await setupPipePane({ screenName, folder, launchId });
|
|
276
|
-
return launchId;
|
|
253
|
+
return pid;
|
|
277
254
|
});
|
|
278
255
|
|
|
279
|
-
/** Links an already-running screen to a process record, creating one when this process has never been recorded - it was
|
|
256
|
+
/** Links an already-running screen to a process record, creating one when this process has never been recorded - it was started before we restarted, or before process records existed at all. Idempotent: a process we already know about is left alone, so the pipe is only attached the first time we adopt it.
|
|
280
257
|
*
|
|
281
|
-
* The screen
|
|
258
|
+
* The screen names the process itself (pane pid + the start time the OS reports for it), so adoption is just writing down what is already there. */
|
|
282
259
|
export async function ensureProcessRecord(config: {
|
|
283
260
|
screenName: string;
|
|
284
261
|
folder: string;
|
|
285
262
|
panePid: string;
|
|
286
|
-
record: Omit<ProcessRecord, "
|
|
263
|
+
record: Omit<ProcessRecord, "pid" | "startTime" | "folder" | "screenName">;
|
|
287
264
|
}): Promise<void> {
|
|
288
|
-
let
|
|
265
|
+
let pid = config.panePid;
|
|
266
|
+
let startTime = await getProcessStartTime(pid);
|
|
289
267
|
if (!startTime) return;
|
|
290
|
-
let
|
|
291
|
-
let existing = (await listProcessRecords()).find(x => x.launchId === launchId && x.folder === config.folder);
|
|
268
|
+
let existing = await readProcessRecord(config.folder, pid, startTime);
|
|
292
269
|
if (existing) {
|
|
293
270
|
// A takeover renames the session, so the record follows the name it now runs under
|
|
294
271
|
if (existing.screenName === config.screenName) return;
|
|
295
272
|
await writeProcessRecord({ ...existing, screenName: config.screenName });
|
|
296
273
|
return;
|
|
297
274
|
}
|
|
298
|
-
console.log(`Adopting already-running process ${
|
|
275
|
+
console.log(`Adopting already-running process ${pid} on screen ${config.screenName}, which has no record yet`);
|
|
299
276
|
await writeProcessRecord({
|
|
300
277
|
...config.record,
|
|
301
|
-
|
|
278
|
+
pid,
|
|
279
|
+
startTime,
|
|
302
280
|
folder: config.folder,
|
|
303
281
|
screenName: config.screenName,
|
|
304
|
-
pid: parseInt(config.panePid) || undefined,
|
|
305
|
-
startTime,
|
|
306
282
|
});
|
|
307
283
|
// Its output was going to whatever the previous incarnation pointed at (or nowhere), so point it at its own log from here on
|
|
308
|
-
await setupPipePane({ screenName: config.screenName, folder: config.folder,
|
|
284
|
+
await setupPipePane({ screenName: config.screenName, folder: config.folder, pid, startTime });
|
|
309
285
|
}
|
|
310
286
|
|
|
311
|
-
// Points the screen's pipe-pane at this
|
|
312
|
-
async function setupPipePane(config: { screenName: string; folder: string;
|
|
287
|
+
// Points the screen's pipe-pane at this process's own log file.
|
|
288
|
+
async function setupPipePane(config: { screenName: string; folder: string; pid: string; startTime: number }) {
|
|
313
289
|
let prefix = getTmuxPrefix();
|
|
314
|
-
let logPath = getProcessLogPath(config.folder, config.
|
|
315
|
-
let pipeScript = getPipeScriptPath(config.folder, config.
|
|
290
|
+
let logPath = getProcessLogPath(config.folder, config.pid, config.startTime);
|
|
291
|
+
let pipeScript = getPipeScriptPath(config.folder, config.pid, config.startTime);
|
|
316
292
|
await fs.promises.mkdir(path.dirname(logPath), { recursive: true });
|
|
317
293
|
await fs.promises.writeFile(pipeScript, getLogPipeScript(logPath));
|
|
318
294
|
await runPromise(`chmod +x ${pipeScript}`);
|
|
@@ -320,42 +296,61 @@ async function setupPipePane(config: { screenName: string; folder: string; launc
|
|
|
320
296
|
}
|
|
321
297
|
|
|
322
298
|
/** Retires whatever is running under the canonical name and renames the future screen onto it. Everything it needs is derived from the two names, so it costs two `has-session` checks - the kill time is when this runs, not when a resync gets around to it. Returns whether there was a future screen to take over. */
|
|
323
|
-
export async function takeoverScreen(config: {
|
|
324
|
-
|
|
299
|
+
export async function takeoverScreen(config: {
|
|
300
|
+
canonicalScreenName: string;
|
|
301
|
+
folder: string;
|
|
302
|
+
/** The node the version being replaced registered as, read before the replacement overwrote the folder's file. */
|
|
303
|
+
replacedNodeId?: string;
|
|
304
|
+
}): Promise<boolean> {
|
|
305
|
+
let { canonicalScreenName, folder, replacedNodeId } = config;
|
|
325
306
|
return await withScreenLock(canonicalScreenName, async () => {
|
|
307
|
+
let futurePid = await getScreenPanePid(getFutureScreenName(canonicalScreenName));
|
|
308
|
+
if (!futurePid) return false;
|
|
326
309
|
let futureScreenName = getFutureScreenName(canonicalScreenName);
|
|
327
|
-
if (!await screenExists(futureScreenName)) return false;
|
|
328
310
|
let prefix = getTmuxPrefix();
|
|
329
|
-
|
|
311
|
+
let outgoingPid = await getScreenPanePid(canonicalScreenName);
|
|
312
|
+
if (outgoingPid) {
|
|
313
|
+
// Moved aside rather than waited on, so the canonical name is free at once and the old process winds down on its own
|
|
330
314
|
let retiringScreenName = getRetiringScreenName(canonicalScreenName, Date.now());
|
|
331
|
-
// Its record carries the node it registered as, taken before the replacement started - the file in the folder belongs to the replacement now, so it is left alone
|
|
332
|
-
let outgoing = await findRunningRecord(folder, canonicalScreenName);
|
|
333
315
|
await runPromise(`${prefix}tmux rename-session -t ${canonicalScreenName} ${retiringScreenName}`);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
316
|
+
await renameProcessScreen({ folder, panePid: outgoingPid, screenName: retiringScreenName });
|
|
317
|
+
if (replacedNodeId) {
|
|
318
|
+
// The file in the folder belongs to the replacement now, so only the registration goes
|
|
319
|
+
void removeServiceNode({ folder, nodeId: replacedNodeId, ownsFile: false });
|
|
339
320
|
}
|
|
340
|
-
|
|
341
|
-
void killScreen({ screenName: retiringScreenName })
|
|
321
|
+
retiringScreens.add(retiringScreenName);
|
|
322
|
+
void killScreen({ screenName: retiringScreenName }).finally(() => {
|
|
323
|
+
retiringScreens.delete(retiringScreenName);
|
|
324
|
+
});
|
|
342
325
|
}
|
|
343
|
-
console.log(green(`Renaming
|
|
326
|
+
console.log(green(`Renaming ${futureScreenName} to ${canonicalScreenName} (takeover complete)`));
|
|
344
327
|
await runPromise(`${prefix}tmux rename-session -t ${futureScreenName} ${canonicalScreenName}`);
|
|
345
|
-
|
|
346
|
-
if (incoming) {
|
|
347
|
-
await writeProcessRecord({ ...incoming, screenName: canonicalScreenName });
|
|
348
|
-
}
|
|
328
|
+
await renameProcessScreen({ folder, panePid: futurePid, screenName: canonicalScreenName });
|
|
349
329
|
return true;
|
|
350
330
|
});
|
|
351
331
|
}
|
|
352
332
|
|
|
333
|
+
/** Points a process's record at the session name it now runs under. The pid is the key, so this reads exactly one file. */
|
|
334
|
+
async function renameProcessScreen(config: { folder: string; panePid: string; screenName: string }): Promise<void> {
|
|
335
|
+
let startTime = await getProcessStartTime(config.panePid);
|
|
336
|
+
if (!startTime) return;
|
|
337
|
+
let record = await readProcessRecord(config.folder, config.panePid, startTime);
|
|
338
|
+
if (!record || record.screenName === config.screenName) return;
|
|
339
|
+
await writeProcessRecord({ ...record, screenName: config.screenName });
|
|
340
|
+
}
|
|
341
|
+
|
|
353
342
|
// canonical screen name -> the takeover we already have scheduled for it
|
|
354
343
|
const scheduledTakeovers = new Map<string, { time: number; cancel: () => void }>();
|
|
355
344
|
|
|
356
345
|
/** Schedules the takeover for the moment the old process's overlap runs out. The resync loop still does the same takeover whenever it notices the time has passed, which is what recovers this if we restart and lose the timer. */
|
|
357
|
-
export function scheduleTakeover(config: {
|
|
358
|
-
|
|
346
|
+
export function scheduleTakeover(config: {
|
|
347
|
+
canonicalScreenName: string;
|
|
348
|
+
folder: string;
|
|
349
|
+
killTime: number;
|
|
350
|
+
/** The node the version being replaced registered as, read before the replacement overwrote the folder's file. */
|
|
351
|
+
replacedNodeId?: string;
|
|
352
|
+
}): void {
|
|
353
|
+
let { canonicalScreenName, folder, killTime, replacedNodeId } = config;
|
|
359
354
|
let existing = scheduledTakeovers.get(canonicalScreenName);
|
|
360
355
|
if (existing?.time === killTime) return;
|
|
361
356
|
existing?.cancel();
|
|
@@ -364,12 +359,18 @@ export function scheduleTakeover(config: { canonicalScreenName: string; folder:
|
|
|
364
359
|
time: killTime,
|
|
365
360
|
callback: () => {
|
|
366
361
|
scheduledTakeovers.delete(canonicalScreenName);
|
|
367
|
-
void takeoverScreen({ canonicalScreenName, folder });
|
|
362
|
+
void takeoverScreen({ canonicalScreenName, folder, replacedNodeId });
|
|
368
363
|
},
|
|
369
364
|
});
|
|
370
365
|
scheduledTakeovers.set(canonicalScreenName, { time: killTime, cancel });
|
|
371
366
|
}
|
|
372
367
|
|
|
368
|
+
// The screens whose shutdown is already running. Nothing else should start a second one, but a screen left behind by a restart is NOT in here, so the unused-screen sweep still cleans it up.
|
|
369
|
+
const retiringScreens = new Set<string>();
|
|
370
|
+
export function isRetiring(screenName: string): boolean {
|
|
371
|
+
return retiringScreens.has(screenName);
|
|
372
|
+
}
|
|
373
|
+
|
|
373
374
|
// Kills a screen and nothing else. Which node the process registered as, and whether that registration should be dropped, is the deploy logic's business - it reads the node id off the process's record, which was taken before any replacement could overwrite the file.
|
|
374
375
|
export const killScreen = measureWrap(async function killScreen(config: {
|
|
375
376
|
screenName: string;
|
|
@@ -385,10 +386,11 @@ export const killScreen = measureWrap(async function killScreen(config: {
|
|
|
385
386
|
/** Streams one process's log: everything already in it, then everything appended after. One process, one file - nothing here has to reason about which process a byte came from. */
|
|
386
387
|
export async function streamProcessOutput(config: {
|
|
387
388
|
folder: string;
|
|
388
|
-
|
|
389
|
+
pid: string;
|
|
390
|
+
startTime: number;
|
|
389
391
|
onData: (data: string) => Promise<void>;
|
|
390
392
|
}) {
|
|
391
|
-
let logPath = getProcessLogPath(config.folder, config.
|
|
393
|
+
let logPath = getProcessLogPath(config.folder, config.pid, config.startTime);
|
|
392
394
|
let serialOnData = runInSerial(config.onData);
|
|
393
395
|
let stopped = false;
|
|
394
396
|
let childProcess: ChildProcess | undefined;
|
|
@@ -407,14 +409,14 @@ export async function streamProcessOutput(config: {
|
|
|
407
409
|
const onDataWrapped = async (data: string) => {
|
|
408
410
|
pendingDataCalls++;
|
|
409
411
|
if (pendingDataCalls > MAX_PENDING_CALLS) {
|
|
410
|
-
console.error(`Too many queued onData calls for ${config.
|
|
412
|
+
console.error(`Too many queued onData calls for ${config.pid}, stopping stream.`);
|
|
411
413
|
await stop();
|
|
412
414
|
return;
|
|
413
415
|
}
|
|
414
416
|
try {
|
|
415
417
|
await serialOnData(data);
|
|
416
418
|
} catch (e: any) {
|
|
417
|
-
console.log(`Callback for stream output ${config.
|
|
419
|
+
console.log(`Callback for stream output ${config.pid} failed. It probably just disconnected, almost certainly not an error: ${e.message}`);
|
|
418
420
|
await stop();
|
|
419
421
|
} finally {
|
|
420
422
|
pendingDataCalls--;
|
|
@@ -422,7 +424,7 @@ export async function streamProcessOutput(config: {
|
|
|
422
424
|
};
|
|
423
425
|
|
|
424
426
|
try {
|
|
425
|
-
let tailScript = getTailScriptPath(config.folder, config.
|
|
427
|
+
let tailScript = getTailScriptPath(config.folder, config.pid, config.startTime);
|
|
426
428
|
await fs.promises.writeFile(tailScript, getLogTailScript(logPath));
|
|
427
429
|
await runPromise(`chmod +x ${tailScript}`);
|
|
428
430
|
|