querysub 0.558.0 → 0.560.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/package.json +2 -2
- package/src/deployManager/components/ProcessesView.tsx +8 -2
- package/src/deployManager/components/RouteConfigView.tsx +6 -20
- package/src/deployManager/components/StoragePage.tsx +36 -1
- package/src/deployManager/components/Tag.tsx +26 -0
- package/src/deployManager/machineApplyMainCode.ts +46 -1
- package/src/deployManager/processManager.ts +33 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.560.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
72
72
|
"pako": "^2.1.0",
|
|
73
73
|
"peggy": "^5.0.6",
|
|
74
|
-
"sliftutils": "^1.7.
|
|
74
|
+
"sliftutils": "^1.7.41",
|
|
75
75
|
"socket-function": "^1.2.26",
|
|
76
76
|
"terser": "^5.31.0",
|
|
77
77
|
"typenode": "^6.6.1",
|
|
@@ -8,7 +8,7 @@ import { sort, timeInSecond, nextId } from "socket-function/src/misc";
|
|
|
8
8
|
import { MachineController, watchProcessOutput, stopWatchingProcessOutput } from "../machineController";
|
|
9
9
|
import type { ProcessRecord } from "../processLogs";
|
|
10
10
|
import { Button } from "../../library-components/Button";
|
|
11
|
-
import { parseAnsiColors } from "../../diagnostics/logs/ansiFormat";
|
|
11
|
+
import { parseAnsiColors, rgbToHsl } from "../../diagnostics/logs/ansiFormat";
|
|
12
12
|
|
|
13
13
|
module.hotreload = true;
|
|
14
14
|
|
|
@@ -17,6 +17,8 @@ const DEAD_COLOR = { h: 0, s: 0, l: 92 };
|
|
|
17
17
|
const OUTPUT_BUFFER_LIMIT = 1_000_000;
|
|
18
18
|
const OUTPUT_BUFFER_KEPT = 100_000;
|
|
19
19
|
const OUTPUT_MAX_HEIGHT = "40vh";
|
|
20
|
+
const ANSI_SATURATION = 70;
|
|
21
|
+
const ANSI_LIGHTNESS = 70;
|
|
20
22
|
|
|
21
23
|
/** One process's live output. Mounting starts the stream, unmounting stops it, so the watch lifetime is exactly the time it is on screen. */
|
|
22
24
|
class ProcessOutput extends qreact.Component<{ record: ProcessRecord; machineNodeId: string }> {
|
|
@@ -70,7 +72,11 @@ class ProcessOutput extends qreact.Component<{ record: ProcessRecord; machineNod
|
|
|
70
72
|
.maxHeight(OUTPUT_MAX_HEIGHT).overflow("auto").pad2(10, 8)
|
|
71
73
|
.hsl(0, 0, 12).colorhsl(0, 0, 90)
|
|
72
74
|
}>
|
|
73
|
-
{parseAnsiColors(this.state.data)}
|
|
75
|
+
{parseAnsiColors(this.state.data).map(({ text, color }) => {
|
|
76
|
+
if (!color) return <span>{text}</span>;
|
|
77
|
+
// The pane is dark, so the ansi colour only sets the hue - the lightness has to stay readable against it
|
|
78
|
+
return <span className={css.colorhsl(rgbToHsl(color).h, ANSI_SATURATION, ANSI_LIGHTNESS)}>{text}</span>;
|
|
79
|
+
})}
|
|
74
80
|
</div>;
|
|
75
81
|
}
|
|
76
82
|
}
|
|
@@ -12,6 +12,7 @@ import type { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig } fr
|
|
|
12
12
|
import { showModal } from "../../5-diagnostics/Modal";
|
|
13
13
|
import { FullscreenModal } from "../../5-diagnostics/FullscreenModal";
|
|
14
14
|
import { Button } from "../../library-components/Button";
|
|
15
|
+
import { Tag, TAG_FONT_SIZE, TAG_WARNING_COLOR, NO_FULL_SYNC_TAG } from "./Tag";
|
|
15
16
|
import { StorageSynced, type StorageServerBuckets } from "./StoragePage";
|
|
16
17
|
|
|
17
18
|
module.hotreload = true;
|
|
@@ -19,8 +20,7 @@ module.hotreload = true;
|
|
|
19
20
|
type Source = HostedConfig | BackblazeConfig;
|
|
20
21
|
|
|
21
22
|
const DEFAULT_HTTPS_PORT = 443;
|
|
22
|
-
const
|
|
23
|
-
const WARNING_COLOR = { h: 35, s: 90, l: 85 };
|
|
23
|
+
const WARNING_COLOR = TAG_WARNING_COLOR;
|
|
24
24
|
const NOTICE_COLOR = { h: 205, s: 70, l: 88 };
|
|
25
25
|
const SOURCE_BORDER_COLOR = { h: 0, s: 0, l: 75 };
|
|
26
26
|
const ACTIVE_COLOR = { h: 130, s: 55, l: 85 };
|
|
@@ -38,7 +38,6 @@ const WINDOW_TIME_LIGHTNESS = 12;
|
|
|
38
38
|
const BUCKET_TITLE_SIZE = 15;
|
|
39
39
|
const GROUP_BODY_INDENT = 64;
|
|
40
40
|
const WINDOW_BODY_INDENT = 64;
|
|
41
|
-
const TAG_FONT_SIZE = 11;
|
|
42
41
|
const JSON_INDENT = 4;
|
|
43
42
|
|
|
44
43
|
/** String sources are the shorthand for an always-valid, unsharded backblaze bucket. */
|
|
@@ -130,7 +129,7 @@ type WindowCluster = {
|
|
|
130
129
|
sources: Source[];
|
|
131
130
|
};
|
|
132
131
|
|
|
133
|
-
/** Every distinct valid window in the config, latest-
|
|
132
|
+
/** Every distinct valid window in the config, latest-starting first, each listing every source that overlaps it. A source spanning several windows appears under each of them - what matters is what is valid during a window, not which window a source "belongs" to. Sources keep their config order, since that order decides which one shadows which. */
|
|
134
133
|
function getWindowRanges(sources: Source[]): WindowCluster[] {
|
|
135
134
|
let byKey = new Map<string, [number, number]>();
|
|
136
135
|
for (let source of sources) {
|
|
@@ -139,9 +138,9 @@ function getWindowRanges(sources: Source[]): WindowCluster[] {
|
|
|
139
138
|
byKey.set(key, [source.validWindow[0], source.validWindow[1]]);
|
|
140
139
|
}
|
|
141
140
|
let windows = [...byKey.values()];
|
|
142
|
-
// Sorted by
|
|
143
|
-
sort(windows, x => -x[0]);
|
|
141
|
+
// Sorted by end first, so the (stable) sort by start leaves later ends first within one start time
|
|
144
142
|
sort(windows, x => -x[1]);
|
|
143
|
+
sort(windows, x => -x[0]);
|
|
145
144
|
return windows.map(window => ({
|
|
146
145
|
window,
|
|
147
146
|
sources: sources.filter(x => windowsOverlap(x.validWindow, window)),
|
|
@@ -344,24 +343,11 @@ function showConfigModal(bucketName: string, rawConfig: RemoteConfig): void {
|
|
|
344
343
|
});
|
|
345
344
|
}
|
|
346
345
|
|
|
347
|
-
class Tag extends qreact.Component<{ icon: string; text: string; warning?: boolean; color?: { h: number; s: number; l: number }; title?: string; }> {
|
|
348
|
-
render() {
|
|
349
|
-
let color = this.props.color || this.props.warning && WARNING_COLOR || TAG_COLOR;
|
|
350
|
-
return <div className={
|
|
351
|
-
css.hbox(4).pad2(6, 1).fontSize(TAG_FONT_SIZE).whiteSpace("nowrap")
|
|
352
|
-
.hsl(color.h, color.s, color.l).bord2(color.h, color.s, color.l - 20)
|
|
353
|
-
} title={this.props.title}>
|
|
354
|
-
<span>{this.props.icon}</span>
|
|
355
|
-
<span>{this.props.text}</span>
|
|
356
|
-
</div>;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
346
|
function getSourceTags(source: Source): { icon: string; text: string }[] {
|
|
361
347
|
let tags: { icon: string; text: string }[] = [];
|
|
362
348
|
if (source.public) tags.push({ icon: "🌐", text: "public" });
|
|
363
349
|
if (source.immutable) tags.push({ icon: "🔒", text: "immutable" });
|
|
364
|
-
if (source.noFullSync) tags.push(
|
|
350
|
+
if (source.noFullSync) tags.push(NO_FULL_SYNC_TAG);
|
|
365
351
|
if (source.intermediate) tags.push({ icon: "⏳", text: "intermediate" });
|
|
366
352
|
if (source.type === "remote") {
|
|
367
353
|
if (source.fast) tags.push({ icon: "⚡", text: "fast" });
|
|
@@ -7,7 +7,8 @@ import preact from "preact";
|
|
|
7
7
|
import { Querysub } from "../../4-querysub/Querysub";
|
|
8
8
|
import { listServerBuckets, clearServerWriteStats, activateServerBucket, getServerActiveBucket } from "sliftutils/storage/remoteStorage/createArchives";
|
|
9
9
|
import type { ServerBucketInfo, BucketDiskInfo, BucketWriteStats, ActiveBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
|
|
10
|
-
import type { ArchivesConfig, SyncActivity } from "sliftutils/storage/IArchives";
|
|
10
|
+
import type { ArchivesConfig, SyncActivity, RemoteConfig, HostedConfig } from "sliftutils/storage/IArchives";
|
|
11
|
+
import { parseHostedUrl } from "sliftutils/storage/remoteStorage/remoteConfig";
|
|
11
12
|
import { UsageBar, getUsageThresholds } from "../../library-components/UsageBar";
|
|
12
13
|
import { getSyncedController } from "../../library-components/SyncedController";
|
|
13
14
|
import { assertIsManagementUser } from "../../diagnostics/managementPages";
|
|
@@ -16,6 +17,7 @@ import { isDefined } from "../../misc";
|
|
|
16
17
|
import { STORAGE_ACCOUNT } from "../../-a-archives/archives2";
|
|
17
18
|
import { Table } from "../../5-diagnostics/Table";
|
|
18
19
|
import { RouteConfigView } from "./RouteConfigView";
|
|
20
|
+
import { Tag, NO_FULL_SYNC_TAG } from "./Tag";
|
|
19
21
|
|
|
20
22
|
module.hotreload = true;
|
|
21
23
|
|
|
@@ -132,6 +134,7 @@ type BucketRow = {
|
|
|
132
134
|
state: string;
|
|
133
135
|
files: string;
|
|
134
136
|
bytes: string;
|
|
137
|
+
noFullSync: boolean;
|
|
135
138
|
indexSources: ArchivesConfig["indexSources"];
|
|
136
139
|
readerDiskLimit: string;
|
|
137
140
|
writes: string;
|
|
@@ -144,10 +147,35 @@ type BucketRow = {
|
|
|
144
147
|
};
|
|
145
148
|
|
|
146
149
|
const GAIN_DECIMALS = 1;
|
|
150
|
+
const DEFAULT_HTTPS_PORT = 443;
|
|
147
151
|
const DISK_BAR_TYPE = "DISK";
|
|
148
152
|
const ACTIVE_STATE = "active";
|
|
149
153
|
const INACTIVE_STATE = "inactive";
|
|
150
154
|
|
|
155
|
+
/** Whether this server only caches what is read of the bucket instead of keeping a full copy, read off its own entry in the bucket's routing config. */
|
|
156
|
+
function getNoFullSync(serverUrl: string, bucketName: string, remoteConfig: RemoteConfig | undefined): boolean {
|
|
157
|
+
let host: URL;
|
|
158
|
+
try {
|
|
159
|
+
host = new URL(serverUrl);
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
let selfEntries: HostedConfig[] = [];
|
|
164
|
+
for (let source of remoteConfig?.sources || []) {
|
|
165
|
+
if (typeof source === "string" || source.type !== "remote") continue;
|
|
166
|
+
try {
|
|
167
|
+
let parsed = parseHostedUrl(source.url);
|
|
168
|
+
if (parsed.address !== host.hostname) continue;
|
|
169
|
+
if (String(parsed.port) !== (host.port || String(DEFAULT_HTTPS_PORT))) continue;
|
|
170
|
+
if (parsed.bucketName !== bucketName) continue;
|
|
171
|
+
selfEntries.push(source);
|
|
172
|
+
} catch {
|
|
173
|
+
// A malformed url in the config is not this column's problem
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return selfEntries.some(x => x.noFullSync);
|
|
177
|
+
}
|
|
178
|
+
|
|
151
179
|
/** How much fast-mode coalescing saved: every accepted write over the ones that actually reached a source. */
|
|
152
180
|
function getWriteGain(stats: BucketWriteStats | undefined): string {
|
|
153
181
|
if (!stats) return "";
|
|
@@ -167,6 +195,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
|
|
|
167
195
|
state: "",
|
|
168
196
|
files: "",
|
|
169
197
|
bytes: "",
|
|
198
|
+
noFullSync: false,
|
|
170
199
|
indexSources: undefined,
|
|
171
200
|
readerDiskLimit: "",
|
|
172
201
|
writes: "",
|
|
@@ -196,6 +225,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
|
|
|
196
225
|
state: bucket.active && ACTIVE_STATE || INACTIVE_STATE,
|
|
197
226
|
files: config?.index && formatNumber(config.index.fileCount) || "",
|
|
198
227
|
bytes: config?.index && formatNumber(config.index.byteCount) + "B" || "",
|
|
228
|
+
noFullSync: getNoFullSync(server.url, bucket.bucketName, config?.remoteConfig),
|
|
199
229
|
indexSources: config?.indexSources,
|
|
200
230
|
readerDiskLimit: config?.readerDiskLimit && formatNumber(config.readerDiskLimit) + "B" || "",
|
|
201
231
|
writes: bucket.writeStats && formatNumber(bucket.writeStats.originalWrites) || "",
|
|
@@ -551,6 +581,11 @@ export class StoragePage extends qreact.Component {
|
|
|
551
581
|
},
|
|
552
582
|
files: { title: "Files" },
|
|
553
583
|
bytes: { title: "Bytes" },
|
|
584
|
+
noFullSync: {
|
|
585
|
+
title: "Sync",
|
|
586
|
+
// Nothing to show when a full copy is kept - that is the default, and the tag is the flag
|
|
587
|
+
formatter: noFullSync => noFullSync && <Tag icon={NO_FULL_SYNC_TAG.icon} text={NO_FULL_SYNC_TAG.text} /> || ""
|
|
588
|
+
},
|
|
554
589
|
indexSources: {
|
|
555
590
|
title: "Index sources",
|
|
556
591
|
formatter: sources => <IndexSourcesCell sources={sources} />
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { qreact } from "../../4-dom/qreact";
|
|
2
|
+
import { css } from "typesafecss";
|
|
3
|
+
|
|
4
|
+
module.hotreload = true;
|
|
5
|
+
|
|
6
|
+
export type TagColor = { h: number; s: number; l: number };
|
|
7
|
+
|
|
8
|
+
export const TAG_COLOR: TagColor = { h: 210, s: 45, l: 88 };
|
|
9
|
+
export const TAG_WARNING_COLOR: TagColor = { h: 35, s: 90, l: 85 };
|
|
10
|
+
export const TAG_FONT_SIZE = 11;
|
|
11
|
+
|
|
12
|
+
/** A source flag, shown wherever that flag matters. The tag is only ever rendered when the flag is on - an absent tag is the default, so there is nothing to show for it. */
|
|
13
|
+
export const NO_FULL_SYNC_TAG = { icon: "⇣", text: "noFullSync" };
|
|
14
|
+
|
|
15
|
+
export class Tag extends qreact.Component<{ icon: string; text: string; warning?: boolean; color?: TagColor; title?: string; }> {
|
|
16
|
+
render() {
|
|
17
|
+
let color = this.props.color || this.props.warning && TAG_WARNING_COLOR || TAG_COLOR;
|
|
18
|
+
return <div className={
|
|
19
|
+
css.hbox(4).pad2(6, 1).fontSize(TAG_FONT_SIZE).whiteSpace("nowrap")
|
|
20
|
+
.hsl(color.h, color.s, color.l).bord2(color.h, color.s, color.l - 20)
|
|
21
|
+
} title={this.props.title}>
|
|
22
|
+
<span>{this.props.icon}</span>
|
|
23
|
+
<span>{this.props.text}</span>
|
|
24
|
+
</div>;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -26,7 +26,7 @@ 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, getTmuxPrefix, getScreenState, isScreenRunningProcess, getProcessStartTime, runScreenCommand, killScreen, streamProcessOutput } from "./processManager";
|
|
29
|
+
import { getScreenName, getFutureScreenName, getTmuxPrefix, getScreenState, isScreenRunningProcess, getProcessStartTime, ensureProcessRecord, runScreenCommand, killScreen, streamProcessOutput } from "./processManager";
|
|
30
30
|
import { ProcessRecord, syncProcessRecords, writeProcessRecord, listProcessRecords, getProcessLogPath } from "./processLogs";
|
|
31
31
|
|
|
32
32
|
|
|
@@ -288,6 +288,19 @@ async function ensureFutureStarted(config: FutureConfig) {
|
|
|
288
288
|
let existing = config.screenStateMap.get(futureScreenName);
|
|
289
289
|
if (existing?.isProcessRunning) {
|
|
290
290
|
config.screenNamesUsed.add(futureScreenName);
|
|
291
|
+
// Started before we restarted, so record it rather than only ever recording processes we launched ourselves
|
|
292
|
+
await ensureProcessRecord({
|
|
293
|
+
screenName: futureScreenName,
|
|
294
|
+
folder: os.homedir() + "/" + SERVICE_FOLDER + canonicalScreenName + "/",
|
|
295
|
+
panePid: existing.pid,
|
|
296
|
+
record: {
|
|
297
|
+
serviceId: config.serviceId,
|
|
298
|
+
serviceKey: config.next.key,
|
|
299
|
+
index: config.index,
|
|
300
|
+
machineId: config.machineId,
|
|
301
|
+
parameters: config.next,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
291
304
|
console.log(green(`Verified future instance ${magenta(futureScreenName)} is running`));
|
|
292
305
|
return;
|
|
293
306
|
}
|
|
@@ -515,6 +528,22 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
515
528
|
if (isPrepTime) {
|
|
516
529
|
// From prep time on, the canonical (old) screen is left completely untouched — the folder holds the NEW code and parameters by now, so the normal compare/sync logic below must not run against the old process
|
|
517
530
|
if (screenStateMap.get(screenName)?.isProcessRunning) {
|
|
531
|
+
// The old instance keeps running untouched through the overlap, but it still needs a record - it may have been launched before we restarted
|
|
532
|
+
let panePid = screenStateMap.get(screenName)?.pid;
|
|
533
|
+
if (panePid) {
|
|
534
|
+
await ensureProcessRecord({
|
|
535
|
+
screenName,
|
|
536
|
+
folder,
|
|
537
|
+
panePid,
|
|
538
|
+
record: {
|
|
539
|
+
serviceId: config.serviceId,
|
|
540
|
+
serviceKey: config.parameters.key,
|
|
541
|
+
index: i,
|
|
542
|
+
machineId,
|
|
543
|
+
parameters: instanceParameters,
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
}
|
|
518
547
|
await syncTimeline(screenStateMap.get(screenName)?.pid);
|
|
519
548
|
let nodePathId = folder + SERVICE_NODE_FILE_NAME;
|
|
520
549
|
if (await fsExistsAsync(nodePathId)) {
|
|
@@ -573,6 +602,22 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
573
602
|
if (prevParameters !== newParametersString) {
|
|
574
603
|
await fs.promises.writeFile(parameterPath, newParametersString);
|
|
575
604
|
}
|
|
605
|
+
// We did not launch this process (we restarted, or it predates process records), so make sure it is recorded before we leave it alone
|
|
606
|
+
let panePid = screenStateMap.get(screenName)?.pid;
|
|
607
|
+
if (panePid) {
|
|
608
|
+
await ensureProcessRecord({
|
|
609
|
+
screenName,
|
|
610
|
+
folder,
|
|
611
|
+
panePid,
|
|
612
|
+
record: {
|
|
613
|
+
serviceId: config.serviceId,
|
|
614
|
+
serviceKey: config.parameters.key,
|
|
615
|
+
index: i,
|
|
616
|
+
machineId,
|
|
617
|
+
parameters: instanceParameters,
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
}
|
|
576
621
|
await syncTimeline(screenStateMap.get(screenName)?.pid);
|
|
577
622
|
console.log(green(`Verified ${magenta(screenName)} is running`));
|
|
578
623
|
continue;
|
|
@@ -11,7 +11,7 @@ import { forceRemoveNode } from "../-f-node-discovery/NodeDiscovery";
|
|
|
11
11
|
import { fsExistsAsync } from "../fs";
|
|
12
12
|
import { PromiseObj } from "../promise";
|
|
13
13
|
import { SERVICE_FOLDER, SERVICE_NODE_FILE_NAME } from "./machineSchema";
|
|
14
|
-
import { ProcessRecord, createLaunchId, getLogPipeScript, getLogTailScript, getProcessLogPath, getPipeScriptPath, getTailScriptPath, writeProcessRecord } from "./processLogs";
|
|
14
|
+
import { ProcessRecord, createLaunchId, getLogPipeScript, getLogTailScript, getProcessLogPath, getPipeScriptPath, getTailScriptPath, listProcessRecords, writeProcessRecord } from "./processLogs";
|
|
15
15
|
|
|
16
16
|
// 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.
|
|
17
17
|
|
|
@@ -244,6 +244,38 @@ ${config.command}
|
|
|
244
244
|
return launchId;
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
+
/** Links an already-running screen to a process record, creating one when this process has never been recorded - it was launched 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.
|
|
248
|
+
*
|
|
249
|
+
* The screen identifies itself (pane pid + the start time the OS reports for it), so adoption is just naming what is already there - there is nothing to reconcile and nothing to guess. */
|
|
250
|
+
export async function ensureProcessRecord(config: {
|
|
251
|
+
screenName: string;
|
|
252
|
+
folder: string;
|
|
253
|
+
panePid: string;
|
|
254
|
+
record: Omit<ProcessRecord, "launchId" | "folder" | "screenName" | "startTime" | "pid">;
|
|
255
|
+
}): Promise<void> {
|
|
256
|
+
let startTime = await getProcessStartTime(config.panePid);
|
|
257
|
+
if (!startTime) return;
|
|
258
|
+
let launchId = createLaunchId(config.panePid, startTime);
|
|
259
|
+
let existing = (await listProcessRecords()).find(x => x.launchId === launchId && x.folder === config.folder);
|
|
260
|
+
if (existing) {
|
|
261
|
+
// A takeover renames the session, so the record follows the name it now runs under
|
|
262
|
+
if (existing.screenName === config.screenName) return;
|
|
263
|
+
await writeProcessRecord({ ...existing, screenName: config.screenName });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
console.log(`Adopting already-running process ${launchId} on screen ${config.screenName}, which has no record yet`);
|
|
267
|
+
await writeProcessRecord({
|
|
268
|
+
...config.record,
|
|
269
|
+
launchId,
|
|
270
|
+
folder: config.folder,
|
|
271
|
+
screenName: config.screenName,
|
|
272
|
+
pid: parseInt(config.panePid) || undefined,
|
|
273
|
+
startTime,
|
|
274
|
+
});
|
|
275
|
+
// Its output was going to whatever the previous incarnation pointed at (or nowhere), so point it at its own log from here on
|
|
276
|
+
await setupPipePane({ screenName: config.screenName, folder: config.folder, launchId });
|
|
277
|
+
}
|
|
278
|
+
|
|
247
279
|
// Points the screen's pipe-pane at this launch's own log file.
|
|
248
280
|
async function setupPipePane(config: { screenName: string; folder: string; launchId: string }) {
|
|
249
281
|
let prefix = getTmuxPrefix();
|