querysub 0.460.0 → 0.462.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/-b-authorities/dnsAuthority.ts +23 -15
- package/src/-g-core-values/NodeCapabilities.ts +3 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +11 -3
- package/src/0-path-value-core/PathRouter.ts +6 -0
- package/src/0-path-value-core/PathWatcher.ts +1 -1
- package/src/0-path-value-core/pathValueCore.ts +4 -7
- package/src/1-path-client/RemoteWatcher.ts +8 -3
- package/src/1-path-client/pathValueClientWatcher.ts +3 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1 -1
- package/src/2-proxy/TransactionDelayer.ts +1 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +13 -8
- package/src/3-path-functions/PathFunctionRunner.ts +2 -0
- package/src/4-querysub/Querysub.ts +0 -1
- package/src/4-querysub/QuerysubController.ts +1 -7
- package/src/config.ts +6 -0
- package/src/config2.ts +7 -1
- package/src/deployManager/components/MachinePicker.tsx +40 -0
- package/src/deployManager/components/ServiceDetailPage.tsx +2 -5
- package/src/deployManager/components/ServicesListPage.tsx +2 -0
- package/src/deployManager/components/Tools.tsx +165 -0
- package/src/deployManager/machineApplyMainCode.ts +2 -2
- package/src/deployManager/setupMachineMain.ts +65 -23
- package/src/diagnostics/charts/Chart.tsx +240 -0
- package/src/diagnostics/grossStats/GrossStatsPage.tsx +48 -83
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +3 -3
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +18 -3
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +1 -0
- package/src/diagnostics/managementPages.tsx +58 -58
- package/src/diagnostics/misc-pages/DNSPage.tsx +344 -0
- package/test.ts +29 -170
- package/src/diagnostics/AuditLogPage.tsx +0 -147
- package/src/diagnostics/NodeConnectionsPage.tsx +0 -167
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.462.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",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
67
67
|
"pako": "^2.1.0",
|
|
68
68
|
"peggy": "^5.0.6",
|
|
69
|
-
"socket-function": "^1.1.
|
|
69
|
+
"socket-function": "^1.1.34",
|
|
70
70
|
"terser": "^5.31.0",
|
|
71
71
|
"typesafecss": "^0.29.0",
|
|
72
72
|
"yaml": "^2.5.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from "os";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import { cache, lazy } from "socket-function/src/caching";
|
|
4
|
-
import { isNode, isNodeTrue } from "socket-function/src/misc";
|
|
4
|
+
import { isNode, isNodeTrue, timeInDay } from "socket-function/src/misc";
|
|
5
5
|
import { httpsRequest } from "../https";
|
|
6
6
|
import { getStorageDir } from "../fs";
|
|
7
7
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
@@ -17,6 +17,8 @@ const DNS_TTLSeconds = {
|
|
|
17
17
|
"A": 60,
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const DNS_REFRESH_STALE_AFTER = timeInDay;
|
|
21
|
+
|
|
20
22
|
export const hasDNSWritePermissions = lazy(async () => {
|
|
21
23
|
if (!isNode()) return false;
|
|
22
24
|
if (isClient()) return false;
|
|
@@ -53,6 +55,7 @@ export async function getRecordsRaw(type: string, key: string) {
|
|
|
53
55
|
name: string;
|
|
54
56
|
content: string;
|
|
55
57
|
proxied: boolean;
|
|
58
|
+
modified_on: string;
|
|
56
59
|
}[]>(`/zones/${zoneId}/dns_records`);
|
|
57
60
|
return results.filter(x => x.type === type && x.name === key);
|
|
58
61
|
}
|
|
@@ -86,7 +89,8 @@ export async function setRecord(type: string, key: string, value: string, proxie
|
|
|
86
89
|
let prevValues = await getRecordsRaw(type, key);
|
|
87
90
|
// NOTE: Apparently if we try to update by just changing proxied, cloudflare complains and
|
|
88
91
|
// says "an identical record already exists", even though it doesn't, we changed the proxied value...
|
|
89
|
-
|
|
92
|
+
let alreadyExisted = prevValues.some(x => x.content === value);
|
|
93
|
+
if (alreadyExisted && Date.now() - new Date(prevValues.find(x => x.content === value)!.modified_on).getTime() < DNS_REFRESH_STALE_AFTER) return;
|
|
90
94
|
|
|
91
95
|
console.log(magenta(`Removing previous records of ${type} for ${key} ${JSON.stringify(prevValues.map(x => x.content))}`));
|
|
92
96
|
let didDeletions = false;
|
|
@@ -104,14 +108,15 @@ export async function setRecord(type: string, key: string, value: string, proxie
|
|
|
104
108
|
ttl,
|
|
105
109
|
proxied: proxied === "proxied",
|
|
106
110
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
if (!alreadyExisted) {
|
|
112
|
+
// NOTE: Apparently... even if the record didn't exist, we still have to wait...
|
|
113
|
+
console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
|
|
114
|
+
for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
|
|
115
|
+
await delay(1000);
|
|
116
|
+
console.log(`${ttlLeft} seconds left...`);
|
|
117
|
+
}
|
|
118
|
+
console.log(`Done waiting for DNS to update.`);
|
|
112
119
|
}
|
|
113
|
-
console.log(`Done waiting for DNS to update.`);
|
|
114
|
-
|
|
115
120
|
}
|
|
116
121
|
/** Keeps existing records */
|
|
117
122
|
export async function addRecord(type: string, key: string, value: string, proxied?: "proxied") {
|
|
@@ -120,7 +125,8 @@ export async function addRecord(type: string, key: string, value: string, proxie
|
|
|
120
125
|
let prevValues = await getRecordsRaw(type, key);
|
|
121
126
|
// NOTE: Apparently if we try to update by just changing proxied, cloudflare complains and
|
|
122
127
|
// says "an identical record already exists", even though it doesn't, we changed the proxied value...
|
|
123
|
-
|
|
128
|
+
let alreadyExisted = prevValues.some(x => x.content === value);
|
|
129
|
+
if (alreadyExisted && Date.now() - new Date(prevValues.find(x => x.content === value)!.modified_on).getTime() < DNS_REFRESH_STALE_AFTER) return;
|
|
124
130
|
console.log(`Adding ${type} record for ${key} to ${value} (previously had ${JSON.stringify(prevValues.map(x => x.content))})`);
|
|
125
131
|
const ttl = DNS_TTLSeconds[type as "A"] || 60;
|
|
126
132
|
await cloudflarePOSTCall(`/zones/${zoneId}/dns_records`, {
|
|
@@ -130,10 +136,12 @@ export async function addRecord(type: string, key: string, value: string, proxie
|
|
|
130
136
|
ttl,
|
|
131
137
|
proxied: proxied === "proxied",
|
|
132
138
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
if (!alreadyExisted) {
|
|
140
|
+
console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
|
|
141
|
+
for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
|
|
142
|
+
await delay(1000);
|
|
143
|
+
console.log(`${ttlLeft} seconds left...`);
|
|
144
|
+
}
|
|
145
|
+
console.log(`Done waiting for DNS to update.`);
|
|
137
146
|
}
|
|
138
|
-
console.log(`Done waiting for DNS to update.`);
|
|
139
147
|
}
|
|
@@ -22,6 +22,7 @@ import { sort } from "socket-function/src/misc";
|
|
|
22
22
|
import { getPathStr2 } from "../path";
|
|
23
23
|
import { PromiseObj } from "../promise";
|
|
24
24
|
import { getTrueTimeOffset } from "socket-function/time/trueTimeShim";
|
|
25
|
+
import { getGitRefLive } from "../4-deploy/git";
|
|
25
26
|
setImmediate(() => {
|
|
26
27
|
import("../diagnostics/MachineThreadInfo");
|
|
27
28
|
});
|
|
@@ -136,6 +137,7 @@ export type NodeMetadata = {
|
|
|
136
137
|
authoritySpec: AuthoritySpec;
|
|
137
138
|
exposedControllers: string[];
|
|
138
139
|
trueTimeOffset: number;
|
|
140
|
+
gitRef: string;
|
|
139
141
|
};
|
|
140
142
|
|
|
141
143
|
class NodeCapabilitiesControllerBase {
|
|
@@ -149,6 +151,7 @@ class NodeCapabilitiesControllerBase {
|
|
|
149
151
|
authoritySpec: authorityLookup.getOurSpec(),
|
|
150
152
|
exposedControllers: Array.from(SocketFunction.exposedClasses),
|
|
151
153
|
trueTimeOffset: getTrueTimeOffset(),
|
|
154
|
+
gitRef: await getGitRefLive(),
|
|
152
155
|
};
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -999,7 +999,9 @@ class PathValueSerializer {
|
|
|
999
999
|
if (pathValue.isValueLazy) {
|
|
1000
1000
|
let buffer = this.lazyValues.get(pathValue.value as {});
|
|
1001
1001
|
if (!buffer) {
|
|
1002
|
-
|
|
1002
|
+
debugbreak(2);
|
|
1003
|
+
debugger;
|
|
1004
|
+
throw new Error(`(getPathValue) Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
|
|
1003
1005
|
}
|
|
1004
1006
|
let newValue = recursiveFreeze(cbor.decode(buffer));
|
|
1005
1007
|
if (!noMutate && !pathValue.isValueLazy) {
|
|
@@ -1024,10 +1026,12 @@ class PathValueSerializer {
|
|
|
1024
1026
|
if (pathValue.isValueLazy) {
|
|
1025
1027
|
let buffer = this.lazyValues.get(pathValue.value as {});
|
|
1026
1028
|
if (!buffer) {
|
|
1029
|
+
debugbreak(2);
|
|
1030
|
+
debugger;
|
|
1027
1031
|
// NOTE: Did you pass a raw PathValue and then try to use PathValueSerializer with it?
|
|
1028
1032
|
// - Instead you should pass a buffer serialized with pathValueSerializer.serialize and
|
|
1029
1033
|
// deserialized with pathValueSerializer.deserialize.
|
|
1030
|
-
throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
|
|
1034
|
+
throw new Error(`(getPathValueOrBuffer) Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
|
|
1031
1035
|
}
|
|
1032
1036
|
return buffer;
|
|
1033
1037
|
}
|
|
@@ -1037,7 +1041,11 @@ class PathValueSerializer {
|
|
|
1037
1041
|
private getBuffer(pathValue: PathValue): Buffer {
|
|
1038
1042
|
if (pathValue.isValueLazy) {
|
|
1039
1043
|
let buffer = this.lazyValues.get(pathValue.value as {});
|
|
1040
|
-
if (!buffer)
|
|
1044
|
+
if (!buffer) {
|
|
1045
|
+
debugbreak(2);
|
|
1046
|
+
debugger;
|
|
1047
|
+
throw new Error(`(getBuffer) Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
|
|
1048
|
+
}
|
|
1041
1049
|
return buffer;
|
|
1042
1050
|
}
|
|
1043
1051
|
return cborEncoder().encode(pathValue.value);
|
|
@@ -421,6 +421,12 @@ export class PathRouter {
|
|
|
421
421
|
return this.matchesAuthoritySpec(ourSpec, path);
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
public static isSelfParentAuthority(path: string): boolean {
|
|
425
|
+
let nodes = PathRouter.getChildReadNodes(path, { onlyOwnNodes: true });
|
|
426
|
+
// NOTE: I think we can just do the node's length is greater than or equal to zero. However, the every check isn't gonna hurt...
|
|
427
|
+
return nodes.nodes.length > 0 && nodes.nodes.every(x => isOwnNodeId(x.nodeId));
|
|
428
|
+
}
|
|
429
|
+
|
|
424
430
|
|
|
425
431
|
|
|
426
432
|
// NOTE: This might return overlapping specs. Presently, this is only used when we're loading our initial data, so it's fine. However, if we use this for another purpose in the future, it might cause problems. So we might need to implement a different function for that theoretical future purpose.
|
|
@@ -192,7 +192,7 @@ class PathWatcher {
|
|
|
192
192
|
if (newPathsWatched.size > 0 || newParentsWatched.size > 0) {
|
|
193
193
|
if (!this.remoteWatchCallback) throw new Error("remoteWatchCallback not set by the pathWatcher");
|
|
194
194
|
let newRemotePaths = Array.from(newPathsWatched).filter(x => !PathRouter.isSelfAuthority(x));
|
|
195
|
-
let newRemoteParentPaths = Array.from(newParentsWatched).filter(x => !PathRouter.
|
|
195
|
+
let newRemoteParentPaths = Array.from(newParentsWatched).filter(x => !PathRouter.isSelfParentAuthority(x));
|
|
196
196
|
if (newRemotePaths.length > 0 || newRemoteParentPaths.length > 0) {
|
|
197
197
|
this.remoteWatchCallback({
|
|
198
198
|
paths: newRemotePaths,
|
|
@@ -667,13 +667,10 @@ class AuthorityPathValueStorage {
|
|
|
667
667
|
}
|
|
668
668
|
|
|
669
669
|
// Self authority check
|
|
670
|
-
{
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
this.parentsSynced.set(originalPath, true);
|
|
675
|
-
return true;
|
|
676
|
-
}
|
|
670
|
+
if (PathRouter.isSelfParentAuthority(originalPath)) {
|
|
671
|
+
// If we're the self authority of it, it's not going to change, so we can just cache it safely.
|
|
672
|
+
this.parentsSynced.set(originalPath, true);
|
|
673
|
+
return true;
|
|
677
674
|
}
|
|
678
675
|
|
|
679
676
|
return false;
|
|
@@ -4,7 +4,7 @@ import { batchFunction, runInSerial, runInfinitePoll } from "socket-function/src
|
|
|
4
4
|
import { cache } from "socket-function/src/caching";
|
|
5
5
|
import { isNode, sort } from "socket-function/src/misc";
|
|
6
6
|
import { measureFnc, measureBlock } from "socket-function/src/profiling/measure";
|
|
7
|
-
import { isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
7
|
+
import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
8
8
|
import { PathValueController } from "../0-path-value-core/PathValueController";
|
|
9
9
|
import { AuthoritySpec, PathRouter } from "../0-path-value-core/PathRouter";
|
|
10
10
|
import { WatchConfig, authorityStorage } from "../0-path-value-core/pathValueCore";
|
|
@@ -164,6 +164,9 @@ export class RemoteWatcher {
|
|
|
164
164
|
// and the code calling this function isn't looking for that specific issue.
|
|
165
165
|
return this.remoteWatchPaths.has(path) || !PathRouter.getReadyAuthority(path);
|
|
166
166
|
}
|
|
167
|
+
public hasAnyDirectPathWatches(path: string) {
|
|
168
|
+
return this.remoteWatchPaths.has(path);
|
|
169
|
+
}
|
|
167
170
|
|
|
168
171
|
/** @deprecated If you want to watch something, call pathWatcher.watchPath instead. */
|
|
169
172
|
public watchLatest(config: WatchConfig & { debugName?: string }) {
|
|
@@ -687,8 +690,10 @@ registerGetSpecForChildPath(path => {
|
|
|
687
690
|
let remoteWatchRange = remoteWatcher.getRemoteWatchParentRange(path);
|
|
688
691
|
if (remoteWatchRange) return remoteWatchRange.authoritySpec;
|
|
689
692
|
|
|
690
|
-
|
|
691
|
-
|
|
693
|
+
if (!remoteWatcher.hasAnyDirectPathWatches(path)) {
|
|
694
|
+
// NOTE: This might mean that our Path Watcher and remote watcher are out of sync, which is a big problem and will break things.
|
|
695
|
+
console.error(`We do not own a path, but also aren't watching it on a remote, but... receive the value? This is weird, and will probably break things (we will likely ignore this value / not trigger parent watches for it).`, { path });
|
|
696
|
+
}
|
|
692
697
|
}
|
|
693
698
|
return authorityLookup.getOurSpec();
|
|
694
699
|
});
|
|
@@ -739,6 +739,9 @@ export class ClientWatcher {
|
|
|
739
739
|
}
|
|
740
740
|
|
|
741
741
|
if (!config.dryRun) {
|
|
742
|
+
for (let pv of pathValues) {
|
|
743
|
+
(globalThis as any).TEMP_TEST_LOG?.(pv.path, { kind: "setValues", writeTime, value: pv.value, locks: pv.locks });
|
|
744
|
+
}
|
|
742
745
|
pathValueCommitter.commitValues(pathValues, config.noWritePrediction ? undefined : "predictWrites");
|
|
743
746
|
}
|
|
744
747
|
|
|
@@ -1491,7 +1491,7 @@ export class PathValueProxyWatcher {
|
|
|
1491
1491
|
}, 60000);
|
|
1492
1492
|
}
|
|
1493
1493
|
watcher.countSinceLastFullSync++;
|
|
1494
|
-
if (watcher.countSinceLastFullSync >
|
|
1494
|
+
if (watcher.countSinceLastFullSync > 100) {
|
|
1495
1495
|
console.warn(`Watcher ${watcher.debugName} has been unsynced for ${watcher.countSinceLastFullSync} times. This is fine, but maybe optimize it. Why is it cascading?`, { lastUnsyncedAccesses: watcher.lastUnsyncedAccesses, lastUnsyncedParentAccesses: watcher.lastUnsyncedParentAccesses }, watcher.options.watchFunction);
|
|
1496
1496
|
}
|
|
1497
1497
|
if (watcher.countSinceLastFullSync > 500) {
|
|
@@ -88,5 +88,5 @@ export function waitIfReceivedIncompleteTransaction(watcher: SyncWatcher) {
|
|
|
88
88
|
// This really nicely both blocks it from finishing because of the waiting for the promise and also triggers it automatically when the delay finishes. HOWEVER, In practice, the promise should never be required to trigger it. It should trigger when we receive the missing parts of the transaction, which we will be, of course, watching as they're in the path values that we're accessing. The timeout is just in case something goes wrong and we incorrectly think we should receive the values, but we won't. That way, eventually, we do commit the value.
|
|
89
89
|
// ALSO! Plus the hash I think only stores like 48 bits. So the chance of collision is somewhat high, especially if we're accessing thousands of paths. So sometimes this will trigger even though we're not missing any part just because we had a collision between the path hashes.
|
|
90
90
|
proxyWatcher.triggerOnPromiseFinish(promise, { waitReason: "Missing transaction part" });
|
|
91
|
-
console.warn(`
|
|
91
|
+
console.warn(`Waiting for missing transaction part ${newestTime.path} which was written at time ${newestTime.time} (now is ${now}). We have parts of this transaction, but we are missing this specific path.`);
|
|
92
92
|
}
|
|
@@ -19,7 +19,8 @@ import { interceptCalls } from "../-0-hooks/hooks";
|
|
|
19
19
|
import { createRoutingOverrideKey } from "../0-path-value-core/PathRouterRouteOverride";
|
|
20
20
|
import { PathRouter } from "../0-path-value-core/PathRouter";
|
|
21
21
|
import { getPathFromProxy } from "../2-proxy/pathValueProxy";
|
|
22
|
-
import { getDomain } from "../config";
|
|
22
|
+
import { getDomain, isPublic } from "../config";
|
|
23
|
+
import { getFncFilter, isServer } from "../config2";
|
|
23
24
|
|
|
24
25
|
// NOTE: We could deploy single functions, but... we will almost always be updating all functions at
|
|
25
26
|
// once, because keeping everything on the same git hash reduces a lot of potential bugs.
|
|
@@ -180,13 +181,17 @@ export function writeFunctionCall(config: {
|
|
|
180
181
|
// this is fine).
|
|
181
182
|
callerIP: "127.0.0.1",
|
|
182
183
|
};
|
|
183
|
-
if
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
// NOTE: I don't know when this would ever really work. I guess to isolate function calls, but if we're running locally, isn't that easy?
|
|
185
|
+
// if (!isNode()) {
|
|
186
|
+
// // Get the "setfncfilter" querystring parameter
|
|
187
|
+
// let url = new URL(window.location.href);
|
|
188
|
+
// let setfncfilter = url.searchParams.get("setfncfilter");
|
|
189
|
+
// if (setfncfilter) {
|
|
190
|
+
// callSpec.filterable = parseFilterable(setfncfilter);
|
|
191
|
+
// }
|
|
192
|
+
// }
|
|
193
|
+
if (isNode()) {
|
|
194
|
+
callSpec.filterable = getFncFilter();
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
if (curInterceptor) {
|
|
@@ -385,6 +385,8 @@ export class PathFunctionRunner {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
if (PathFunctionRunner.DEBUG_CALLS) {
|
|
388
|
+
// NOTE: If this start, it means that the filter didn't match, which either means it was a dev function and so not relevant for us, or we are dev and it's a production function, so not relevant for us.
|
|
389
|
+
// - The filter is set by fncfilter when running serverside, or, clientside, by fncfilter set on the command line of the querysub server.
|
|
388
390
|
console.log(`QUEUING ${getDebugName(callData, functionSpec, true)}`);
|
|
389
391
|
let resultsPath = getProxyPath(() => moduleData.Results[callId]);
|
|
390
392
|
let history = authorityStorage.getValuePlusHistory(resultsPath);
|
|
@@ -1333,7 +1333,6 @@ setImmediate(async () => {
|
|
|
1333
1333
|
// Import, so it registers addStatPeriodic
|
|
1334
1334
|
await import("../5-diagnostics/nodeMetadata");
|
|
1335
1335
|
await import("../diagnostics/pathAuditer");
|
|
1336
|
-
await import("../diagnostics/NodeConnectionsPage");
|
|
1337
1336
|
});
|
|
1338
1337
|
|
|
1339
1338
|
setImmediate(async () => {
|
|
@@ -27,7 +27,7 @@ import { CallerContextBase } from "socket-function/SocketFunctionTypes";
|
|
|
27
27
|
import { isTrustedByNode } from "../-d-trust/NetworkTrust2";
|
|
28
28
|
import { Querysub, id } from "./Querysub";
|
|
29
29
|
import { isDefined } from "../misc";
|
|
30
|
-
import { isClient, isServer } from "../config2";
|
|
30
|
+
import { getFncFilter, isClient, isServer } from "../config2";
|
|
31
31
|
import { PromiseObj } from "../promise";
|
|
32
32
|
import { LoggingClient } from "../0-path-value-core/LoggingClient";
|
|
33
33
|
import * as prediction from "./querysubPrediction";
|
|
@@ -42,12 +42,6 @@ import { PathRouter } from "../0-path-value-core/PathRouter";
|
|
|
42
42
|
import { authorityLookup } from "../0-path-value-core/AuthorityLookup";
|
|
43
43
|
import { PathValueArchives } from "../0-path-value-core/pathValueArchives";
|
|
44
44
|
|
|
45
|
-
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
46
|
-
.option("fncfilter", { type: "string", default: "", desc: `Sets the filterable state for function calls, causing them to target specific FunctionRunners. If no FunctionRunner matches, all functions will fail to run. For example: "devtestserver" will match a FunctionRunner that uses the "devtestserver" filter. Merges with the existing filterable state if a client sets it explicitly.` })
|
|
47
|
-
.argv
|
|
48
|
-
;
|
|
49
|
-
|
|
50
|
-
const getFncFilter = lazy(() => parseFilterable(yargObj.fncfilter));
|
|
51
45
|
|
|
52
46
|
export { Querysub, id };
|
|
53
47
|
|
package/src/config.ts
CHANGED
|
@@ -31,8 +31,14 @@ let yargObj = parseArgsFactory()
|
|
|
31
31
|
})
|
|
32
32
|
.option("logbackblaze", { type: "boolean", desc: "Log all backblaze activity to disk." })
|
|
33
33
|
.option("slowdown", { type: "number", desc: "Delay all input data values by this amount of time, pretending like we didn't even receive it until this time is up." })
|
|
34
|
+
.option("fncfilter", { type: "string", default: "", desc: `Sets the filterable state for function calls, causing them to target specific FunctionRunners. If no FunctionRunner matches, all functions will fail to run. For example: "devtestserver" will match a FunctionRunner that uses the "devtestserver" filter. Merges with the existing filterable state if a client sets it explicitly.` })
|
|
34
35
|
.argv
|
|
35
36
|
;
|
|
37
|
+
|
|
38
|
+
export function getRawFncFilter() {
|
|
39
|
+
return yargObj.fncfilter;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
type QuerysubConfig = {
|
|
37
43
|
domain?: string;
|
|
38
44
|
emaildomain?: string;
|
package/src/config2.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { deepCloneJSON, isNode } from "socket-function/src/misc";
|
|
2
2
|
import { hasArchivesPermissions } from "./-a-archives/archives";
|
|
3
|
-
import { baseIsClient, getDomain } from "./config";
|
|
3
|
+
import { baseIsClient, getDomain, getRawFncFilter } from "./config";
|
|
4
4
|
import { JSONLACKS } from "socket-function/src/JSONLACKS/JSONLACKS";
|
|
5
5
|
import { rootPathStr, prependToPathStr, getPathDepth } from "./path";
|
|
6
6
|
import fs from "fs";
|
|
7
7
|
import { AuthoritySpec } from "./0-path-value-core/PathRouter";
|
|
8
|
+
import { parseFilterable } from "./misc/filterable";
|
|
9
|
+
import { lazy } from "socket-function/src/caching";
|
|
8
10
|
|
|
9
11
|
let isInCall = false;
|
|
10
12
|
export function isClient() {
|
|
@@ -21,3 +23,7 @@ export function isServer() {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// NOTE: getOurAuthorities moved to PathRouterHashOverrides, and renamed to getOurAuthoritySpec
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export const getFncFilter = lazy(() => parseFilterable(getRawFncFilter() || ""));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
3
|
+
import { qreact } from "../../4-dom/qreact";
|
|
4
|
+
import { sort } from "socket-function/src/misc";
|
|
5
|
+
import { isDefined } from "../../misc";
|
|
6
|
+
import { formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
7
|
+
import { InputPicker } from "../../library-components/InputPicker";
|
|
8
|
+
import { MachineServiceController } from "../machineSchema";
|
|
9
|
+
|
|
10
|
+
module.hotreload = true;
|
|
11
|
+
|
|
12
|
+
export class MachinePicker extends qreact.Component<{
|
|
13
|
+
label?: preact.ComponentChild;
|
|
14
|
+
picked: string[];
|
|
15
|
+
addPicked: (machineId: string) => void;
|
|
16
|
+
removePicked: (machineId: string) => void;
|
|
17
|
+
singleOption?: boolean;
|
|
18
|
+
fillWidth?: boolean;
|
|
19
|
+
}> {
|
|
20
|
+
render() {
|
|
21
|
+
let controller = MachineServiceController(SocketFunction.browserNodeId());
|
|
22
|
+
let machines = (controller.getMachineList() || []).map(x => controller.getMachineInfo(x)).filter(isDefined);
|
|
23
|
+
sort(machines, x => -x.heartbeat);
|
|
24
|
+
|
|
25
|
+
let options = machines.map(machineObj => ({
|
|
26
|
+
value: machineObj.machineId,
|
|
27
|
+
label: `${machineObj.machineId} ${machineObj.info["getExternalIP"]} (${Object.keys(machineObj.services || {}).length} services, last heartbeat ${formatVeryNiceDateTime(machineObj.heartbeat)})`,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
return <InputPicker<string>
|
|
31
|
+
label={this.props.label}
|
|
32
|
+
picked={this.props.picked}
|
|
33
|
+
options={options}
|
|
34
|
+
addPicked={this.props.addPicked}
|
|
35
|
+
removePicked={this.props.removePicked}
|
|
36
|
+
singleOption={this.props.singleOption}
|
|
37
|
+
fillWidth={this.props.fillWidth}
|
|
38
|
+
/>;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -7,6 +7,7 @@ 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 { MachinePicker } from "./MachinePicker";
|
|
10
11
|
import { deepCloneJSON, nextId, sort } from "socket-function/src/misc";
|
|
11
12
|
import { InputLabel } from "../../library-components/InputLabel";
|
|
12
13
|
import { Button } from "../../library-components/Button";
|
|
@@ -291,13 +292,9 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
291
292
|
/>
|
|
292
293
|
</div>
|
|
293
294
|
|
|
294
|
-
<
|
|
295
|
+
<MachinePicker
|
|
295
296
|
label="Machines"
|
|
296
297
|
picked={config.machineIds}
|
|
297
|
-
options={machines.map(machineObj => ({
|
|
298
|
-
value: machineObj.machineId,
|
|
299
|
-
label: `${machineObj.machineId} ${machineObj.info["getExternalIP"]} (${Object.keys(machineObj.services || {}).length} services, last heartbeat ${formatVeryNiceDateTime(machineObj.heartbeat)})`
|
|
300
|
-
}))}
|
|
301
298
|
addPicked={machineId => {
|
|
302
299
|
configT.machineIds.push(machineId);
|
|
303
300
|
this.updateUnsavedChanges(configT);
|
|
@@ -13,6 +13,7 @@ import { isPublic } from "../../config";
|
|
|
13
13
|
import { UpdateButtons, UpdateServiceButtons } from "./deployButtons";
|
|
14
14
|
import { isDefined } from "../../misc";
|
|
15
15
|
import { formatDateJSX } from "../../misc/formatJSX";
|
|
16
|
+
import { Tools } from "./Tools";
|
|
16
17
|
|
|
17
18
|
module.hotreload = true;
|
|
18
19
|
|
|
@@ -74,6 +75,7 @@ export class ServicesListPage extends qreact.Component {
|
|
|
74
75
|
</button>
|
|
75
76
|
<UpdateButtons services={services.map(x => x[1]).filter(isDefined)} />
|
|
76
77
|
</div>
|
|
78
|
+
<Tools />
|
|
77
79
|
<div className={css.vbox(8)}>
|
|
78
80
|
{services.map(([serviceId, config]) => {
|
|
79
81
|
if (!config) return <div key={serviceId}>Config is broken? Missing value for service? Is the file corrupted?</div>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
3
|
+
import { qreact } from "../../4-dom/qreact";
|
|
4
|
+
import { css } from "typesafecss";
|
|
5
|
+
import { t } from "../../2-proxy/schema2";
|
|
6
|
+
import { Querysub } from "../../4-querysub/QuerysubController";
|
|
7
|
+
import { MachineServiceController, ServiceConfig } from "../machineSchema";
|
|
8
|
+
import { MachinePicker } from "./MachinePicker";
|
|
9
|
+
import { isDefined } from "../../misc";
|
|
10
|
+
|
|
11
|
+
module.hotreload = true;
|
|
12
|
+
|
|
13
|
+
type ToolEntry = {
|
|
14
|
+
key: string;
|
|
15
|
+
title: string;
|
|
16
|
+
render: () => preact.ComponentChild;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class Tools extends qreact.Component {
|
|
20
|
+
state = t.state({
|
|
21
|
+
expanded: t.atomic<boolean>(false),
|
|
22
|
+
expandedTools: t.lookup(t.boolean),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
let tools: ToolEntry[] = [
|
|
27
|
+
{
|
|
28
|
+
key: "rename-machine",
|
|
29
|
+
title: "Rename Machine",
|
|
30
|
+
render: () => <RenameMachine />,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
return <div className={css.vbox(8)}>
|
|
35
|
+
<div
|
|
36
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 95)}
|
|
37
|
+
onClick={() => {
|
|
38
|
+
this.state.expanded = !this.state.expanded;
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<span className={css.boldStyle}>{this.state.expanded ? "▼" : "▶"} Tools</span>
|
|
42
|
+
</div>
|
|
43
|
+
{this.state.expanded && <div className={css.vbox(6).paddingLeft(12)}>
|
|
44
|
+
{tools.map(tool => {
|
|
45
|
+
let isExpanded = !!this.state.expandedTools[tool.key];
|
|
46
|
+
return <div className={css.vbox(6)} key={tool.key}>
|
|
47
|
+
<div
|
|
48
|
+
className={css.pad2(10, 6).button.bord2(0, 0, 20).hsl(0, 0, 98)}
|
|
49
|
+
onClick={() => {
|
|
50
|
+
if (this.state.expandedTools[tool.key]) {
|
|
51
|
+
delete this.state.expandedTools[tool.key];
|
|
52
|
+
} else {
|
|
53
|
+
this.state.expandedTools[tool.key] = true;
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<span>{isExpanded ? "▼" : "▶"} {tool.title}</span>
|
|
58
|
+
</div>
|
|
59
|
+
{isExpanded && <div className={css.pad2(10).bord2(0, 0, 20).hsl(0, 0, 100)}>
|
|
60
|
+
{tool.render()}
|
|
61
|
+
</div>}
|
|
62
|
+
</div>;
|
|
63
|
+
})}
|
|
64
|
+
</div>}
|
|
65
|
+
</div>;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class RenameMachine extends qreact.Component {
|
|
70
|
+
state = t.state({
|
|
71
|
+
fromMachineId: t.string,
|
|
72
|
+
toMachineId: t.string,
|
|
73
|
+
isRunning: t.atomic<boolean>(false),
|
|
74
|
+
resultMessage: t.string,
|
|
75
|
+
errorMessage: t.string,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
render() {
|
|
79
|
+
let controller = MachineServiceController(SocketFunction.browserNodeId());
|
|
80
|
+
|
|
81
|
+
let { fromMachineId, toMachineId } = this.state;
|
|
82
|
+
let canRun = !!fromMachineId && !!toMachineId && fromMachineId !== toMachineId && !this.state.isRunning;
|
|
83
|
+
|
|
84
|
+
return <div className={css.vbox(10)}>
|
|
85
|
+
<div className={css.colorhsl(0, 0, 40)}>
|
|
86
|
+
Rewrites every service config: any occurrence of "From" in its <code>machineIds</code> becomes "To".
|
|
87
|
+
</div>
|
|
88
|
+
<MachinePicker
|
|
89
|
+
label="From"
|
|
90
|
+
singleOption
|
|
91
|
+
picked={fromMachineId ? [fromMachineId] : []}
|
|
92
|
+
addPicked={(value) => { this.state.fromMachineId = value; }}
|
|
93
|
+
removePicked={() => { this.state.fromMachineId = ""; }}
|
|
94
|
+
/>
|
|
95
|
+
<MachinePicker
|
|
96
|
+
label="To"
|
|
97
|
+
singleOption
|
|
98
|
+
picked={toMachineId ? [toMachineId] : []}
|
|
99
|
+
addPicked={(value) => { this.state.toMachineId = value; }}
|
|
100
|
+
removePicked={() => { this.state.toMachineId = ""; }}
|
|
101
|
+
/>
|
|
102
|
+
<div className={css.hbox(8)}>
|
|
103
|
+
<button
|
|
104
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20)
|
|
105
|
+
+ (canRun ? css.hsl(0, 0, 100) : css.hsl(0, 0, 85).colorhsl(0, 0, 50))}
|
|
106
|
+
disabled={!canRun}
|
|
107
|
+
onClick={() => {
|
|
108
|
+
if (!canRun) return;
|
|
109
|
+
let from = fromMachineId;
|
|
110
|
+
let to = toMachineId;
|
|
111
|
+
let confirmed = confirm(`Rename machine "${from}" → "${to}" across all service configs?\n\nThis updates every ServiceConfig that references "${from}".`);
|
|
112
|
+
if (!confirmed) return;
|
|
113
|
+
|
|
114
|
+
Querysub.commit(() => {
|
|
115
|
+
this.state.isRunning = true;
|
|
116
|
+
this.state.resultMessage = "";
|
|
117
|
+
this.state.errorMessage = "";
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
Querysub.onCommitFinished(async () => {
|
|
121
|
+
try {
|
|
122
|
+
let serviceIds = await controller.getServiceList.promise();
|
|
123
|
+
let configs = await Promise.all(
|
|
124
|
+
serviceIds.map(id => controller.getServiceConfig.promise(id))
|
|
125
|
+
);
|
|
126
|
+
let toUpdate: ServiceConfig[] = [];
|
|
127
|
+
for (let config of configs.filter(isDefined)) {
|
|
128
|
+
if (!config.machineIds.includes(from)) continue;
|
|
129
|
+
let newMachineIds = config.machineIds.map(m => m === from ? to : m);
|
|
130
|
+
toUpdate.push({
|
|
131
|
+
...config,
|
|
132
|
+
machineIds: newMachineIds,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (toUpdate.length === 0) {
|
|
136
|
+
Querysub.commit(() => {
|
|
137
|
+
this.state.resultMessage = `No service configs referenced "${from}".`;
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
await controller.setServiceConfigs.promise(toUpdate);
|
|
141
|
+
Querysub.commit(() => {
|
|
142
|
+
this.state.resultMessage = `Updated ${toUpdate.length} service config(s).`;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`RenameMachine failed:`, (err as Error).stack ?? err);
|
|
147
|
+
Querysub.commit(() => {
|
|
148
|
+
this.state.errorMessage = (err as Error).stack ?? String(err);
|
|
149
|
+
});
|
|
150
|
+
} finally {
|
|
151
|
+
Querysub.commit(() => {
|
|
152
|
+
this.state.isRunning = false;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{this.state.isRunning ? "Renaming..." : "Rename"}
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
{this.state.resultMessage && <div className={css.colorhsl(120, 50, 30)}>{this.state.resultMessage}</div>}
|
|
162
|
+
{this.state.errorMessage && <pre className={css.colorhsl(0, 60, 40).whiteSpace("pre-wrap")}>{this.state.errorMessage}</pre>}
|
|
163
|
+
</div>;
|
|
164
|
+
}
|
|
165
|
+
}
|