querysub 0.234.0 → 0.236.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/ServiceDetailPage.tsx +105 -7
- package/src/deployManager/machineApplyMainCode.ts +90 -2
- package/src/deployManager/machineController.ts +160 -0
- package/src/deployManager/machineSchema.ts +2 -38
- package/src/deployManager/spec.txt +25 -9
- package/src/diagnostics/periodic.ts +13 -16
- package/src/library-components/ScrollOnMount.tsx +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.236.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",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"js-sha512": "^0.9.0",
|
|
23
23
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
24
24
|
"pako": "^2.1.0",
|
|
25
|
-
"socket-function": "^0.
|
|
25
|
+
"socket-function": "^0.133.0",
|
|
26
26
|
"terser": "^5.31.0",
|
|
27
27
|
"typesafecss": "^0.22.0",
|
|
28
28
|
"yaml": "^2.5.0",
|
|
@@ -7,10 +7,13 @@ import { Querysub } from "../../4-querysub/QuerysubController";
|
|
|
7
7
|
import { currentViewParam, selectedServiceIdParam, selectedMachineIdParam } from "../urlParams";
|
|
8
8
|
import { formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
9
9
|
import { InputPicker } from "../../library-components/InputPicker";
|
|
10
|
-
import { sort } from "socket-function/src/misc";
|
|
10
|
+
import { nextId, sort } from "socket-function/src/misc";
|
|
11
11
|
import { InputLabel } from "../../library-components/InputLabel";
|
|
12
12
|
import { Button } from "../../library-components/Button";
|
|
13
13
|
import { isDefined } from "../../misc";
|
|
14
|
+
import { watchScreenOutput, stopWatchingScreenOutput } from "../machineController";
|
|
15
|
+
import { getPathStr2 } from "../../path";
|
|
16
|
+
import { ScrollOnMount } from "../../library-components/ScrollOnMount";
|
|
14
17
|
|
|
15
18
|
// Type declarations for Monaco editor
|
|
16
19
|
declare global {
|
|
@@ -25,12 +28,17 @@ declare global {
|
|
|
25
28
|
|
|
26
29
|
export class ServiceDetailPage extends qreact.Component {
|
|
27
30
|
state = t.state({
|
|
28
|
-
unsavedChanges: t.
|
|
29
|
-
isSaving: t.
|
|
31
|
+
unsavedChanges: t.type<ServiceConfig | undefined>(undefined),
|
|
32
|
+
isSaving: t.type(false),
|
|
30
33
|
saveError: t.string,
|
|
31
34
|
expandedErrors: t.lookup({
|
|
32
|
-
expanded: t.
|
|
33
|
-
})
|
|
35
|
+
expanded: t.type(false)
|
|
36
|
+
}),
|
|
37
|
+
watchingOutputs: t.lookup({
|
|
38
|
+
isWatching: t.type(false),
|
|
39
|
+
data: t.type(""),
|
|
40
|
+
callbackId: t.type(""),
|
|
41
|
+
}),
|
|
34
42
|
});
|
|
35
43
|
|
|
36
44
|
private async ensureMonacoLoaded(): Promise<void> {
|
|
@@ -95,6 +103,54 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
95
103
|
void this.updateConfig(updatedConfig);
|
|
96
104
|
});
|
|
97
105
|
}
|
|
106
|
+
|
|
107
|
+
private async startWatchingOutput(config: {
|
|
108
|
+
nodeId: string;
|
|
109
|
+
key: string;
|
|
110
|
+
index: number;
|
|
111
|
+
}) {
|
|
112
|
+
const { nodeId, key, index } = config;
|
|
113
|
+
const outputKey = getPathStr2(key, index + "");
|
|
114
|
+
let callbackId = nextId();
|
|
115
|
+
|
|
116
|
+
Querysub.commit(() => {
|
|
117
|
+
this.state.watchingOutputs[outputKey] = { isWatching: true, data: "", callbackId };
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await watchScreenOutput({
|
|
122
|
+
nodeId,
|
|
123
|
+
key,
|
|
124
|
+
index,
|
|
125
|
+
callbackId,
|
|
126
|
+
onData: async (data: string) => {
|
|
127
|
+
Querysub.localCommit(() => {
|
|
128
|
+
let fullData = this.state.watchingOutputs[outputKey].data + data;
|
|
129
|
+
fullData = fullData.slice(-10000);
|
|
130
|
+
this.state.watchingOutputs[outputKey].data = fullData;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
} catch (error: any) {
|
|
135
|
+
Querysub.localCommit(() => {
|
|
136
|
+
this.state.watchingOutputs[outputKey].data = `Error watching output: ${error.stack || String(error)}`;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async stopWatchingOutput(key: string, index: number) {
|
|
142
|
+
const outputKey = getPathStr2(key, index + "");
|
|
143
|
+
|
|
144
|
+
let callbackId = Querysub.localRead(() => {
|
|
145
|
+
const watchingState = this.state.watchingOutputs[outputKey];
|
|
146
|
+
watchingState.isWatching = false;
|
|
147
|
+
return watchingState.callbackId;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
Querysub.onCommitFinished(async () => {
|
|
151
|
+
await stopWatchingScreenOutput({ callbackId });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
98
154
|
private async updateConfig(updatedConfig: ServiceConfig): Promise<void> {
|
|
99
155
|
const selectedServiceId = selectedServiceIdParam.value;
|
|
100
156
|
if (!selectedServiceId) return;
|
|
@@ -150,7 +206,10 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
150
206
|
const hasUnsavedChanges = !!this.state.unsavedChanges;
|
|
151
207
|
|
|
152
208
|
// Sort machines by status and heartbeat
|
|
209
|
+
let nextIndexes = new Map<string, number>();
|
|
153
210
|
let machineStatuses = config.machineIds.map(machineId => {
|
|
211
|
+
let index = nextIndexes.get(machineId) || 0;
|
|
212
|
+
nextIndexes.set(machineId, index + 1);
|
|
154
213
|
let machineInfo = controller.getMachineInfo(machineId);
|
|
155
214
|
const serviceInfo = machineInfo?.services[selectedServiceId || ""];
|
|
156
215
|
const isMachineDead = machineInfo ? Date.now() - machineInfo.heartbeat > (MACHINE_RESYNC_INTERVAL * 4) : true;
|
|
@@ -162,7 +221,8 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
162
221
|
serviceInfo,
|
|
163
222
|
isMachineDead,
|
|
164
223
|
hasError,
|
|
165
|
-
heartbeat: machineInfo?.heartbeat || 0
|
|
224
|
+
heartbeat: machineInfo?.heartbeat || 0,
|
|
225
|
+
index
|
|
166
226
|
};
|
|
167
227
|
});
|
|
168
228
|
sort(machineStatuses, x => (x.hasError ? 0 : x.isMachineDead ? 1 : 2) * 1000000 + x.heartbeat);
|
|
@@ -247,7 +307,7 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
247
307
|
{configT.parameters.deploy && <div className={css.vbox(8)}>
|
|
248
308
|
<h3>Deployed Machines ({config.machineIds.length})</h3>
|
|
249
309
|
<div className={css.vbox(4)}>
|
|
250
|
-
{machineStatuses.map(({ machineId, machineInfo, serviceInfo, isMachineDead, hasError }) => {
|
|
310
|
+
{machineStatuses.map(({ machineId, machineInfo, serviceInfo, isMachineDead, hasError, index }) => {
|
|
251
311
|
if (!machineInfo) return <div key={machineId}>Loading {machineId}...</div>;
|
|
252
312
|
|
|
253
313
|
let backgroundColor = css.hsl(0, 0, 100); // Default: white
|
|
@@ -259,6 +319,11 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
259
319
|
backgroundColor = css.hsl(30, 70, 90); // Orange for not deployed
|
|
260
320
|
}
|
|
261
321
|
|
|
322
|
+
let key = config.parameters.key;
|
|
323
|
+
const outputKey = getPathStr2(key, index + "");
|
|
324
|
+
const isWatching = this.state.watchingOutputs[outputKey].isWatching;
|
|
325
|
+
const outputData = this.state.watchingOutputs[outputKey].data;
|
|
326
|
+
|
|
262
327
|
return <div key={machineId}
|
|
263
328
|
className={css.pad2(12).button.bord2(0, 0, 20) + backgroundColor}
|
|
264
329
|
onClick={() => {
|
|
@@ -297,6 +362,25 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
297
362
|
Not Running
|
|
298
363
|
</div>
|
|
299
364
|
)}
|
|
365
|
+
|
|
366
|
+
<div
|
|
367
|
+
className={css.pad2(8, 4).button}
|
|
368
|
+
onClick={(e) => {
|
|
369
|
+
e.stopPropagation();
|
|
370
|
+
let applyNodeId = machineInfo.applyNodeId;
|
|
371
|
+
Querysub.onCommitFinished(() => {
|
|
372
|
+
if (isWatching) {
|
|
373
|
+
void this.stopWatchingOutput(key, index);
|
|
374
|
+
} else {
|
|
375
|
+
void this.startWatchingOutput({ nodeId: applyNodeId, key, index });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
<div className={css.pad2(4, 8).bord2(0, 0, 10) + (isWatching ? css.hsl(0, 70, 90) : css.hsl(120, 70, 90))}>
|
|
381
|
+
{isWatching ? "Stop Watching Output" : "Watch Screen Output"}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
300
384
|
</div>
|
|
301
385
|
{hasError && (
|
|
302
386
|
<div
|
|
@@ -324,6 +408,20 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
324
408
|
)}
|
|
325
409
|
</div>
|
|
326
410
|
)}
|
|
411
|
+
|
|
412
|
+
{isWatching && (
|
|
413
|
+
<div
|
|
414
|
+
className={
|
|
415
|
+
css.pad2(8).bord2(0, 0, 10).hsl(0, 0, 10).colorhsl(0, 0, 100)
|
|
416
|
+
.whiteSpace("pre-wrap").fontFamily("monospace")
|
|
417
|
+
.overflowAuto
|
|
418
|
+
.maxHeight("60vh")
|
|
419
|
+
}
|
|
420
|
+
>
|
|
421
|
+
{outputData && outputData || "Waiting for output..."}
|
|
422
|
+
<ScrollOnMount debugText={`Screen ${outputKey}`} />
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
327
425
|
</div>;
|
|
328
426
|
})}
|
|
329
427
|
</div>
|
|
@@ -4,7 +4,7 @@ import { measureWrap } from "socket-function/src/profiling/measure";
|
|
|
4
4
|
import { getOwnMachineId } from "../-a-auth/certs";
|
|
5
5
|
import { getOurNodeId, getOurNodeIdAssert } from "../-f-node-discovery/NodeDiscovery";
|
|
6
6
|
import { Querysub } from "../4-querysub/QuerysubController";
|
|
7
|
-
import { MACHINE_RESYNC_INTERVAL, DeployControllerBase, MachineInfo, ServiceConfig,
|
|
7
|
+
import { MACHINE_RESYNC_INTERVAL, DeployControllerBase, MachineInfo, ServiceConfig, serviceConfigs, SERVICE_FOLDER, machineInfos } from "./machineSchema";
|
|
8
8
|
import { runPromise } from "../functional/runCommand";
|
|
9
9
|
import { getExternalIP } from "socket-function/src/networking";
|
|
10
10
|
import { errorToUndefined, errorToUndefinedSilent } from "../errors";
|
|
@@ -16,10 +16,13 @@ import { logLoadTime } from "../logModuleLoadTimes";
|
|
|
16
16
|
import { delay, runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
17
17
|
import os from "os";
|
|
18
18
|
import fs from "fs";
|
|
19
|
+
import { spawn, ChildProcess } from "child_process";
|
|
19
20
|
import { lazy } from "socket-function/src/caching";
|
|
20
21
|
import { getGitRefLive, setGitRef } from "../4-deploy/git";
|
|
21
|
-
import { blue, green, magenta } from "socket-function/src/formatting/logColors";
|
|
22
|
+
import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
|
|
22
23
|
import { shutdown } from "../diagnostics/periodic";
|
|
24
|
+
import { onServiceConfigChange } from "./machineController";
|
|
25
|
+
import { PromiseObj } from "../promise";
|
|
23
26
|
|
|
24
27
|
const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
|
|
25
28
|
let machineInfo: MachineInfo = {
|
|
@@ -43,6 +46,91 @@ function getScreenName(config: { serviceKey: string; index: number }): string {
|
|
|
43
46
|
return `${config.serviceKey}-${config.index}${SCREEN_SUFFIX}`.replace(/[^a-zA-Z0-9\-_]/g, "_");
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
export async function streamScreenOutput(config: {
|
|
50
|
+
key: string;
|
|
51
|
+
index: number;
|
|
52
|
+
onData: (data: string) => Promise<void>;
|
|
53
|
+
}) {
|
|
54
|
+
let screenName = getScreenName({
|
|
55
|
+
serviceKey: config.key,
|
|
56
|
+
index: config.index,
|
|
57
|
+
});
|
|
58
|
+
let serialOnData = runInSerial(config.onData);
|
|
59
|
+
let stopped = false;
|
|
60
|
+
let childProcess: ChildProcess | undefined;
|
|
61
|
+
const prefix = getTmuxPrefix();
|
|
62
|
+
|
|
63
|
+
let pendingDataCalls = 0;
|
|
64
|
+
const MAX_PENDING_CALLS = 100;
|
|
65
|
+
|
|
66
|
+
const onDataWrapped = async (data: string) => {
|
|
67
|
+
pendingDataCalls++;
|
|
68
|
+
if (pendingDataCalls > MAX_PENDING_CALLS) {
|
|
69
|
+
console.error(`Too many queued onData calls for ${screenName}, stopping stream.`);
|
|
70
|
+
await stop();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await serialOnData(data);
|
|
75
|
+
} catch (e: any) {
|
|
76
|
+
console.log(`Callback for stream output ${screenName} failed. It probably just disconnected, almost certainly not an error: ${e.stack}`);
|
|
77
|
+
await stop();
|
|
78
|
+
} finally {
|
|
79
|
+
pendingDataCalls--;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
async function stop() {
|
|
84
|
+
if (stopped) return;
|
|
85
|
+
stopped = true;
|
|
86
|
+
|
|
87
|
+
if (childProcess) {
|
|
88
|
+
childProcess.kill();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (async () => {
|
|
93
|
+
try {
|
|
94
|
+
const captureCommand = `${prefix}tmux capture-pane -p -S -2000 -t ${screenName}`;
|
|
95
|
+
// Initial data
|
|
96
|
+
const initialContent = await runPromise(captureCommand, { quiet: true });
|
|
97
|
+
if (initialContent) {
|
|
98
|
+
await onDataWrapped(initialContent);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
childProcess = spawn(`${prefix}tmux pipe-pane -t ${screenName}`, {
|
|
102
|
+
shell: true,
|
|
103
|
+
stdio: "pipe",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let started = new PromiseObj<void>();
|
|
107
|
+
|
|
108
|
+
childProcess.stdout?.on("data", (data) => {
|
|
109
|
+
if (stopped) return;
|
|
110
|
+
started.resolve();
|
|
111
|
+
void onDataWrapped(data.toString());
|
|
112
|
+
});
|
|
113
|
+
// Give it some time to error out, otherwise, just start
|
|
114
|
+
setTimeout(() => started.resolve(), 200);
|
|
115
|
+
|
|
116
|
+
childProcess.stderr?.on("data", (data) => {
|
|
117
|
+
if (stopped) return;
|
|
118
|
+
void onDataWrapped(red(data.toString()));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
childProcess.on("error", async (err) => {
|
|
122
|
+
if (stopped) return;
|
|
123
|
+
started.reject(err);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await started.promise;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
void stop();
|
|
129
|
+
throw e;
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
}
|
|
133
|
+
|
|
46
134
|
|
|
47
135
|
const getTmuxPrefix = lazy(() => {
|
|
48
136
|
if (os.platform() === "win32") {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { runInSerial, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
3
|
+
import { lazy } from "socket-function/src/caching";
|
|
4
|
+
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
5
|
+
import { timeInMinute } from "socket-function/src/misc";
|
|
6
|
+
import { assertIsManagementUser } from "../diagnostics/managementPages";
|
|
7
|
+
import { isNode } from "typesafecss";
|
|
8
|
+
import { streamScreenOutput } from "./machineApplyMainCode";
|
|
9
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
10
|
+
import { getPathStr2 } from "../path";
|
|
11
|
+
|
|
12
|
+
const POLL_INTERVAL = timeInMinute * 15;
|
|
13
|
+
|
|
14
|
+
// Only works if our self id has been registered as applyNodeId (with setMachineInfo).
|
|
15
|
+
let serviceConfigChangeWatchers = new Set<() => Promise<void>>();
|
|
16
|
+
|
|
17
|
+
let triggerServiceConfigChangeCallbacks = runInSerial(async () => {
|
|
18
|
+
for (let callback of serviceConfigChangeWatchers) {
|
|
19
|
+
await callback();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
let setupPoll = lazy(() => {
|
|
23
|
+
void runInfinitePollCallAtStart(POLL_INTERVAL, triggerServiceConfigChangeCallbacks);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// NOTE: The callback should block until the changes are applied (possibly throwing). This way the change results can be deployed and when the changer function finishes it will automatically reload them.
|
|
27
|
+
export function onServiceConfigChange(callback: () => Promise<void>): () => void {
|
|
28
|
+
serviceConfigChangeWatchers.add(callback);
|
|
29
|
+
setupPoll();
|
|
30
|
+
return () => {
|
|
31
|
+
serviceConfigChangeWatchers.delete(callback);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
class OnServiceChangeBase {
|
|
35
|
+
public async onServiceConfigChange() {
|
|
36
|
+
await triggerServiceConfigChangeCallbacks();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export const OnServiceChange = SocketFunction.register(
|
|
40
|
+
"on-service-change-aa6b4aaa-c325-4112-b2a8-f81c180016a0",
|
|
41
|
+
() => new OnServiceChangeBase(),
|
|
42
|
+
() => ({
|
|
43
|
+
onServiceConfigChange: {},
|
|
44
|
+
}),
|
|
45
|
+
() => ({
|
|
46
|
+
hooks: [requiresNetworkTrustHook],
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MachineControllerBase {
|
|
52
|
+
// NOTE: We don't need to worry about escaping commands here. YES, the user CAN inject code into the key. But this system is literally for running arbitrary commands, so they could just write a serviceConfig and run anything they want, on all the machines...
|
|
53
|
+
public async streamScreenOutput(config: {
|
|
54
|
+
key: string;
|
|
55
|
+
index: number;
|
|
56
|
+
callbackId: string;
|
|
57
|
+
}): Promise<void> {
|
|
58
|
+
let caller = SocketFunction.getCaller();
|
|
59
|
+
await streamScreenOutput({
|
|
60
|
+
key: config.key,
|
|
61
|
+
index: config.index,
|
|
62
|
+
onData: async (data) => {
|
|
63
|
+
await MachineControllerClient.nodes[caller.nodeId].onScreenOutput({
|
|
64
|
+
key: config.key,
|
|
65
|
+
index: config.index,
|
|
66
|
+
data,
|
|
67
|
+
callbackId: config.callbackId,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// We need to forward it, as clients can't connect to servers directly (as they don't have real HTTP certificates).
|
|
73
|
+
public async watchOtherScreenOutput(config: {
|
|
74
|
+
nodeId: string;
|
|
75
|
+
key: string;
|
|
76
|
+
index: number;
|
|
77
|
+
callbackId: string;
|
|
78
|
+
}) {
|
|
79
|
+
let caller = SocketFunction.getCaller();
|
|
80
|
+
forwardedCallbacks.set(config.callbackId, caller.nodeId);
|
|
81
|
+
await MachineController.nodes[config.nodeId].streamScreenOutput({
|
|
82
|
+
key: config.key,
|
|
83
|
+
index: config.index,
|
|
84
|
+
callbackId: config.callbackId,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let forwardedCallbacks = new Map<string, string>();
|
|
90
|
+
|
|
91
|
+
const MachineController = SocketFunction.register(
|
|
92
|
+
"machine-controller-c3157d4a-580c-4e76-9dc9-072dd92e70af",
|
|
93
|
+
() => new MachineControllerBase(),
|
|
94
|
+
() => ({
|
|
95
|
+
streamScreenOutput: {},
|
|
96
|
+
watchOtherScreenOutput: {},
|
|
97
|
+
}),
|
|
98
|
+
() => ({
|
|
99
|
+
hooks: [assertIsManagementUser],
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
let callbacks = new Map<string, (data: string) => Promise<void>>();
|
|
104
|
+
export async function watchScreenOutput(config: {
|
|
105
|
+
nodeId: string;
|
|
106
|
+
key: string;
|
|
107
|
+
index: number;
|
|
108
|
+
callbackId: string;
|
|
109
|
+
onData: (data: string) => Promise<void>;
|
|
110
|
+
}) {
|
|
111
|
+
let callbackId = config.callbackId;
|
|
112
|
+
callbacks.set(callbackId, config.onData);
|
|
113
|
+
await MachineController.nodes[SocketFunction.browserNodeId()].watchOtherScreenOutput({
|
|
114
|
+
nodeId: config.nodeId,
|
|
115
|
+
key: config.key,
|
|
116
|
+
index: config.index,
|
|
117
|
+
callbackId,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function stopWatchingScreenOutput(config: {
|
|
122
|
+
callbackId: string;
|
|
123
|
+
}) {
|
|
124
|
+
let callbackId = config.callbackId;
|
|
125
|
+
callbacks.delete(callbackId);
|
|
126
|
+
}
|
|
127
|
+
class MachineControllerClientBase {
|
|
128
|
+
public async onScreenOutput(config: {
|
|
129
|
+
key: string;
|
|
130
|
+
index: number;
|
|
131
|
+
data: string;
|
|
132
|
+
callbackId: string;
|
|
133
|
+
}): Promise<void> {
|
|
134
|
+
let forwardToNodeId = forwardedCallbacks.get(config.callbackId);
|
|
135
|
+
if (forwardToNodeId) {
|
|
136
|
+
await MachineControllerClient.nodes[forwardToNodeId].onScreenOutput({
|
|
137
|
+
key: config.key,
|
|
138
|
+
index: config.index,
|
|
139
|
+
data: config.data,
|
|
140
|
+
callbackId: config.callbackId,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let callback = callbacks.get(config.callbackId);
|
|
146
|
+
if (!callback) {
|
|
147
|
+
throw new Error(`Callback ${config.callbackId} not found (likely removed)`);
|
|
148
|
+
}
|
|
149
|
+
await callback(config.data);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// NOTE: THis is secure, because callbackId is random.
|
|
153
|
+
const MachineControllerClient = SocketFunction.register(
|
|
154
|
+
"machine-controller-client-2bcc6c02-96e4-4521-bbb9-2776e7a14d08",
|
|
155
|
+
() => new MachineControllerClientBase(),
|
|
156
|
+
() => ({
|
|
157
|
+
onScreenOutput: {},
|
|
158
|
+
}),
|
|
159
|
+
() => ({}),
|
|
160
|
+
);
|
|
@@ -14,10 +14,11 @@ import { runInSerial, runInfinitePollCallAtStart } from "socket-function/src/bat
|
|
|
14
14
|
import { lazy } from "socket-function/src/caching";
|
|
15
15
|
import { errorToUndefinedSilent } from "../errors";
|
|
16
16
|
import { getGitRefLive, getGitURLLive } from "../4-deploy/git";
|
|
17
|
+
import { OnServiceChange } from "./machineController";
|
|
17
18
|
|
|
18
19
|
export const SERVICE_FOLDER = "machine-services/";
|
|
19
20
|
export const MACHINE_RESYNC_INTERVAL = timeInMinute * 15;
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
|
|
22
23
|
export type ServiceConfig = {
|
|
23
24
|
/** Just a random id to manage the service */
|
|
@@ -80,43 +81,6 @@ export const serviceConfigs = archiveJSONT<ServiceConfig>(() => nestArchives("ma
|
|
|
80
81
|
export const machineInfos = archiveJSONT<MachineInfo>(() => nestArchives("machines/machine-heartbeats/", getArchivesBackblaze(getDomain())));
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
// Only works if our self id has been registered as applyNodeId (with setMachineInfo).
|
|
84
|
-
let serviceConfigChangeWatchers = new Set<() => Promise<void>>();
|
|
85
|
-
|
|
86
|
-
let triggerCallbacks = runInSerial(async () => {
|
|
87
|
-
for (let callback of serviceConfigChangeWatchers) {
|
|
88
|
-
await callback();
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
let setupPoll = lazy(() => {
|
|
92
|
-
void runInfinitePollCallAtStart(POLL_INTERVAL, triggerCallbacks);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// NOTE: The callback should block until the changes are applied (possibly throwing). This way the change results can be deployed and when the changer function finishes it will automatically reload them.
|
|
96
|
-
export function onServiceConfigChange(callback: () => Promise<void>): () => void {
|
|
97
|
-
serviceConfigChangeWatchers.add(callback);
|
|
98
|
-
setupPoll();
|
|
99
|
-
return () => {
|
|
100
|
-
serviceConfigChangeWatchers.delete(callback);
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
class OnServiceChangeBase {
|
|
104
|
-
public async onServiceConfigChange() {
|
|
105
|
-
await triggerCallbacks();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const OnServiceChange = SocketFunction.register(
|
|
109
|
-
"on-service-change-aa6b4aaa-c325-4112-b2a8-f81c180016a0",
|
|
110
|
-
() => new OnServiceChangeBase(),
|
|
111
|
-
() => ({
|
|
112
|
-
onServiceConfigChange: {},
|
|
113
|
-
}),
|
|
114
|
-
() => ({
|
|
115
|
-
hooks: [requiresNetworkTrustHook],
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
|
|
120
84
|
export class DeployControllerBase {
|
|
121
85
|
|
|
122
86
|
public async getMachineList(): Promise<string[]> {
|
|
@@ -1,31 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
3) Hmm... being able to watch the output for a service would be useful, and... it should be... trivial to do?
|
|
2
|
+
- Buttons per key + index, which we can figure out clientside. Stream results under the button, and we can stream multiple at once?
|
|
3
|
+
- OH, just for superusers, so the browser can connect directly!
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
- Stop all loops
|
|
5
|
-
- If any are running, wait (with timeout, same as with regular handlers), for them to finish
|
|
6
|
-
|
|
7
|
-
4) Kill our testing server
|
|
5
|
+
4) Destroy our testing digital ocean server
|
|
8
6
|
|
|
9
7
|
5) Setup on our regular digital ocean server
|
|
10
8
|
- Remove previous startup.sh, and crontab and kill existing tmux services
|
|
11
9
|
- Setup all the services in the new UI
|
|
12
10
|
- Copy from the previous startup.sh, running the same services
|
|
13
11
|
- Changing the UI if anything is extremely annoying, but... I don't see how it would be...
|
|
12
|
+
tmux send-keys -t server1 "cd ~/cyoa && yarn server-public" Enter
|
|
13
|
+
tmux send-keys -t server2 "cd ~/cyoa && yarn server-public" Enter
|
|
14
|
+
tmux send-keys -t fnc "cd ~/cyoa && yarn function-public --verbosecalls" Enter
|
|
15
|
+
tmux send-keys -t http "cd ~/cyoa && yarn cyoa-public --verbosecalls" Enter
|
|
16
|
+
tmux send-keys -t watch "cd ~/cyoa && yarn gc-watch-public" Enter
|
|
17
|
+
tmux send-keys -t join "cd ~/cyoa && yarn join-public" Enter
|
|
14
18
|
5) Verify the editor works
|
|
15
19
|
|
|
16
20
|
6) Verify PathValueServer gracefully shutdowns, not losing any values (because it delays and flushes writes before shutting down, detecting the ctrl+c).
|
|
17
21
|
|
|
18
22
|
|
|
19
|
-
7) Quick node removal on process crash
|
|
20
|
-
|
|
21
|
-
-
|
|
23
|
+
7) Quick node removal on process crash OR removal
|
|
24
|
+
- In the service, check our parent folder to see if we are in a screen (/machine-services/git/), and then write our nodeId to /machine-services/nodeId
|
|
25
|
+
- If we see a nodeId when we are removing a screen, or killing the service, then delete that nodeId from the nodeId directory (and call tellEveryoneNodesChanges)
|
|
22
26
|
7.1) Verify this by killing a lot of services (the function runner?), by just poking it over and over, verifying the nodes are quickly deleted
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
8) REIMPLEMENT yarn do-update functionality, with UI on the configuration page
|
|
31
|
+
OH, maybe... buttons for querysub, button for the main site, and then buttons on each service to update them!
|
|
32
|
+
YES, and then, a button to do all of that on each service, and a button to do that and on each service.
|
|
33
|
+
- So a lot of buttons. But only 2 components, one for all, and one for a specific service.
|
|
34
|
+
- Endpoints
|
|
35
|
+
- anyQuerysubUnsaved
|
|
36
|
+
- anyAppUnsaved
|
|
37
|
+
- querysubLatestHash
|
|
38
|
+
- appLatestHash (already have this)
|
|
39
|
+
- publish, add, commit, and push querysub (and update app package.json reference)
|
|
40
|
+
- add, commit and push app
|
|
41
|
+
|
|
27
42
|
- On services list page, and individual service page
|
|
28
43
|
- So... maybe the button a component
|
|
44
|
+
- MULTIPLE buttons, to just update the main site, just querysub, or both
|
|
29
45
|
- Button to update on each service where the repoUrl === the repoUrl of the server
|
|
30
46
|
- Only from a non-public server
|
|
31
47
|
- ALSO, button to update all (that match repoUrl)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
1
|
+
import { delay, runInfinitePoll, runInfinitePollCallAtStart, shutdownPolling } from "socket-function/src/batching";
|
|
2
2
|
import { isNode, timeInMinute } from "socket-function/src/misc";
|
|
3
3
|
import { logErrors, timeoutToError } from "../errors";
|
|
4
4
|
import debugbreak from "debugbreak";
|
|
@@ -37,26 +37,23 @@ export async function shutdown() {
|
|
|
37
37
|
console.log(red("Starting shutdown"));
|
|
38
38
|
shuttingDown = true;
|
|
39
39
|
const { authorityStorage } = await import("../0-path-value-core/pathValueCore");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
try {
|
|
41
|
+
await Promise.allSettled([
|
|
42
|
+
...preshutdownHandlers,
|
|
43
|
+
].map(fnc => timeoutToError(timeInMinute, fnc(), () => new Error(`Preshutdown handler ${fnc.name} timed out`))));
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log(`Error on preshutdown handlers`, e);
|
|
46
46
|
}
|
|
47
47
|
try {
|
|
48
|
-
await
|
|
49
|
-
|
|
48
|
+
await Promise.allSettled([
|
|
49
|
+
function authorityStorageShutdown() { return authorityStorage.onShutdown(); },
|
|
50
|
+
nodeDiscoveryShutdown,
|
|
51
|
+
shutdownPolling,
|
|
52
|
+
...shutdownHandlers,
|
|
53
|
+
].map(fnc => timeoutToError(timeInMinute, fnc(), () => new Error(`Shutdown handler ${fnc.name} timed out`))));
|
|
50
54
|
} catch (e) {
|
|
51
55
|
console.log("Error on shutdown", e);
|
|
52
56
|
}
|
|
53
|
-
for (let fnc of shutdownHandlers) {
|
|
54
|
-
try {
|
|
55
|
-
await timeoutToError(timeInMinute, fnc(), () => new Error(`Shutdown handler ${fnc.name} timed out`));
|
|
56
|
-
} catch (e) {
|
|
57
|
-
console.log(`Error on shutdown handler ${fnc.name}`, e);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
57
|
// Wait to allow any logged errors to hopefully be written somewhere?
|
|
61
58
|
await delay(2000);
|
|
62
59
|
process.exit();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { qreact } from "../4-dom/qreact";
|
|
2
|
+
export class ScrollOnMount extends qreact.Component<{ debugText: string }> {
|
|
3
|
+
scrolled = false;
|
|
4
|
+
render() {
|
|
5
|
+
return <div className={"ScrollOnMount"} ref={elem => {
|
|
6
|
+
if (!elem) return;
|
|
7
|
+
if (this.scrolled) return;
|
|
8
|
+
this.scrolled = true;
|
|
9
|
+
console.log(`Scrolled to ${this.props.debugText}`);
|
|
10
|
+
elem.scrollIntoView({ behavior: "instant", block: "center" });
|
|
11
|
+
// HACK: For some reason html has scroll (even though it's contents do not?).
|
|
12
|
+
// So scrollIntoView might scroll it, which breaks things. So... reset it's scroll.
|
|
13
|
+
let htmlElement = document.body.parentElement;
|
|
14
|
+
if (htmlElement) {
|
|
15
|
+
htmlElement.scrollTop = 0;
|
|
16
|
+
}
|
|
17
|
+
}} />;
|
|
18
|
+
}
|
|
19
|
+
}
|