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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.234.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.132.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.atomic<ServiceConfig | undefined>(undefined),
29
- isSaving: t.atomic<boolean>(false),
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.atomic<boolean>(false)
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, onServiceConfigChange, serviceConfigs, SERVICE_FOLDER, machineInfos } from "./machineSchema";
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
- const POLL_INTERVAL = timeInMinute * 15;
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
- NOTE: Yarn
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
- 7.1) Better infinite poll support in shutdown
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 or removal
20
- Detect the nodeId of services (if they have one), and when the service dies, immediately remove "edgenodes/" file, and trigger an update of "edge-nodes-index.json"
21
- - How? Damn it, I have no idea. Maybe... they can determine their screen from their pid and then write to their screen folder? That seems... the best way, even though it's extremely coupling.
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
- for (let fnc of preshutdownHandlers) {
41
- try {
42
- await timeoutToError(timeInMinute, fnc(), () => new Error(`Preshutdown handler ${fnc.name} timed out`));
43
- } catch (e) {
44
- console.log(`Error on preshutdown handler ${fnc.name}`, e);
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 authorityStorage.onShutdown();
49
- await nodeDiscoveryShutdown();
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
+ }