querysub 0.699.0 → 0.700.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/.claude/settings.local.json +2 -1
- package/package.json +1 -1
- package/src/-d-trust/NetworkTrust2.ts +14 -2
- package/src/-g-core-values/NodeCapabilities.ts +6 -7
- package/src/deployManager/components/MachineDetailPage.tsx +17 -3
- package/src/deployManager/components/MachinePicker.tsx +8 -4
- package/src/deployManager/components/MachinesListPage.tsx +12 -5
- package/src/deployManager/components/ServiceDetailPage.tsx +11 -5
- package/src/deployManager/machineApplyMainCode.ts +35 -17
- package/src/deployManager/machineSchema.ts +35 -1
- package/src/deployManager/setupMachineMain.ts +60 -7
- package/src/diagnostics/MachineThreadInfo.tsx +4 -0
- package/src/diagnostics/NodeViewer.tsx +12 -6
- package/src/diagnostics/debugger/mcp-server.ts +8 -2
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { measureWrap } from "socket-function/src/profiling/measure";
|
|
2
|
-
import { getIdentityCA, getMachineId, getOwnMachineId } from "sliftutils/misc/https/certs";
|
|
2
|
+
import { getCommonName, getIdentityCA, getMachineId, getOwnMachineId } from "sliftutils/misc/https/certs";
|
|
3
3
|
import { getArchives2 } from "../-a-archives/archives2";
|
|
4
4
|
import { isNode, throttleFunction, timeInHour, timeInSecond } from "socket-function/src/misc";
|
|
5
5
|
import { SocketFunctionHook } from "socket-function/SocketFunctionTypes";
|
|
@@ -44,7 +44,6 @@ export const requiresNetworkTrustHook: SocketFunctionHook = async config => {
|
|
|
44
44
|
let machineId = IdentityController_getMachineId(caller);
|
|
45
45
|
let trusted = await isTrusted(machineId);
|
|
46
46
|
if (!trusted) {
|
|
47
|
-
devDebugbreak();
|
|
48
47
|
let machineId = IdentityController_getMachineId(caller);
|
|
49
48
|
throw new Error(`Calling machine is not trusted. Caller ${machineId} is not trusted by ${SocketFunction.mountedNodeId} to make call ${config.call.classGuid}.${config.call.functionName}. To gain trust add backblaze permissions (see hasBackblazePermissions) or set --nonetwork.`);
|
|
50
49
|
}
|
|
@@ -128,6 +127,19 @@ export const loadServerCert = cache(async (machineId: string) => {
|
|
|
128
127
|
trustCertificate(certFile);
|
|
129
128
|
});
|
|
130
129
|
|
|
130
|
+
export async function trustMachineCertificate(config: { machineId: string; cert: Buffer }): Promise<boolean> {
|
|
131
|
+
let { machineId, cert } = config;
|
|
132
|
+
let certMachineId = getMachineId(getCommonName(cert), getDomain());
|
|
133
|
+
if (certMachineId !== machineId) {
|
|
134
|
+
throw new Error(`Expected the certificate to be for ${machineId}, it is for ${certMachineId}`);
|
|
135
|
+
}
|
|
136
|
+
if (await archives().get(machineId)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
await archives().set(machineId, cert, { fallbacks: true });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
export const ensureWeAreTrusted = lazy(measureWrap(async () => {
|
|
132
144
|
let machineKeyCert = getIdentityCA(getDomain());
|
|
133
145
|
let machineId = getOwnMachineId(getDomain());
|
|
@@ -175,15 +175,16 @@ class NodeCapabilitiesControllerBase {
|
|
|
175
175
|
return getMeasureBreakdown(range);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
public async getInspectURL() {
|
|
179
|
-
|
|
178
|
+
public async getInspectURL(): Promise<string | undefined> {
|
|
179
|
+
if (!isAssistedDebugConnectionAllowed()) return undefined;
|
|
180
180
|
return await getDebuggerUrl();
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
public async exposeExternalDebugPortOnce(forExternalIP: string) {
|
|
184
|
-
|
|
184
|
+
if (!isAssistedDebugConnectionAllowed()) return undefined;
|
|
185
185
|
// https://notdevtools.com/devtools/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:62448/22895aed-f8da-4dfb-8d50-e432a9c2d827
|
|
186
186
|
let debugURL = await this.getInspectURL();
|
|
187
|
+
if (!debugURL) return undefined;
|
|
187
188
|
|
|
188
189
|
const outerUrl = new URL(debugURL);
|
|
189
190
|
const wsParam = outerUrl.searchParams.get("ws");
|
|
@@ -206,10 +207,8 @@ class NodeCapabilitiesControllerBase {
|
|
|
206
207
|
let lastExposed: (() => void) | undefined;
|
|
207
208
|
|
|
208
209
|
/** The assisted debug connection (opening the inspector, forwarding the port externally, TLS proxying) only exists because public servers can't be reached directly - on a non-public server the debugger can be attached directly, and the forwarding machinery would only add attack surface. */
|
|
209
|
-
export function
|
|
210
|
-
|
|
211
|
-
throw new Error(`Non-public server, connect directly. Don't use this assisted connection code.`);
|
|
212
|
-
}
|
|
210
|
+
export function isAssistedDebugConnectionAllowed() {
|
|
211
|
+
return isPublic();
|
|
213
212
|
}
|
|
214
213
|
|
|
215
214
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { qreact } from "../../4-dom/qreact";
|
|
3
|
-
import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, getMachineIdList, getMachineInfoLive } from "../machineSchema";
|
|
3
|
+
import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, getMachineIdList, getMachineInfoLive, getMachineNameLive, setMachineNameLive } from "../machineSchema";
|
|
4
|
+
import { InputLabel } from "../../library-components/InputLabel";
|
|
4
5
|
import { css } from "typesafecss";
|
|
5
6
|
import { currentViewParam, selectedMachineIdParam, selectedServiceIdParam } from "../urlParams";
|
|
6
7
|
import { formatNumber, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
@@ -29,6 +30,7 @@ export class MachineDetailPage extends qreact.Component {
|
|
|
29
30
|
|
|
30
31
|
const machine = machineInfo;
|
|
31
32
|
const isDisabled = machineConfig?.disabled || false;
|
|
33
|
+
const machineName = getMachineNameLive(selectedMachineId);
|
|
32
34
|
|
|
33
35
|
// Get all service configs that target this machine
|
|
34
36
|
let relevantServiceConfigs = new Map<string, ServiceConfig>();
|
|
@@ -65,7 +67,19 @@ export class MachineDetailPage extends qreact.Component {
|
|
|
65
67
|
|
|
66
68
|
return <div className={css.vbox(16)}>
|
|
67
69
|
<div className={css.hbox(12).pad2(16).bord2(0, 0, 20) + backgroundColor}>
|
|
68
|
-
<
|
|
70
|
+
<div className={css.vbox(4).flexGrow(1)}>
|
|
71
|
+
<InputLabel
|
|
72
|
+
label="Name"
|
|
73
|
+
edit
|
|
74
|
+
fontSize={28}
|
|
75
|
+
value={machineName}
|
|
76
|
+
editValue={machineName || "(unnamed machine)"}
|
|
77
|
+
editClass={css.fontSize(28).fontWeight("bold") + (!machineName && css.colorhsl(0, 0, 45))}
|
|
78
|
+
tooltip="Click to name this machine"
|
|
79
|
+
onChangeValue={value => setMachineNameLive(selectedMachineId, value)}
|
|
80
|
+
/>
|
|
81
|
+
<div className={css.fontSize(13).colorhsl(0, 0, 35)}>{selectedMachineId}</div>
|
|
82
|
+
</div>
|
|
69
83
|
{isMachineDead && <div className={css.colorhsl(0, 80, 60)}>
|
|
70
84
|
⚠️ Machine is likely dead
|
|
71
85
|
</div>}
|
|
@@ -120,7 +134,7 @@ export class MachineDetailPage extends qreact.Component {
|
|
|
120
134
|
<ShowMore className={css.whiteSpace("pre-wrap")} maxHeight={80}>
|
|
121
135
|
{(() => {
|
|
122
136
|
if (typeof value === "object") {
|
|
123
|
-
return <UsageBar label={value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
|
|
137
|
+
return <UsageBar label={value.label || value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
|
|
124
138
|
}
|
|
125
139
|
return value;
|
|
126
140
|
})()}
|
|
@@ -6,7 +6,7 @@ import { isDefined } from "../../misc";
|
|
|
6
6
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
7
7
|
import { css } from "typesafecss";
|
|
8
8
|
import { Querysub } from "../../4-querysub/Querysub";
|
|
9
|
-
import { MachineServiceController, getLiveServiceParameters, getMachineTargets, applyCommandTemplate, getMachineInfoLive } from "../machineSchema";
|
|
9
|
+
import { MachineServiceController, getLiveServiceParameters, getMachineTargets, applyCommandTemplate, getMachineInfoLive, getMachineNameLive } from "../machineSchema";
|
|
10
10
|
import { FUNCTION_RUNNER_COMMAND_PREFIX } from "../serviceCategories";
|
|
11
11
|
|
|
12
12
|
module.hotreload = true;
|
|
@@ -69,6 +69,7 @@ export class MachinePicker extends qreact.Component<{
|
|
|
69
69
|
if (typeof ip !== "string") {
|
|
70
70
|
ip = "";
|
|
71
71
|
}
|
|
72
|
+
let name = getMachineNameLive(machineId);
|
|
72
73
|
let sinceHeartbeat = now - machine.heartbeat;
|
|
73
74
|
let isLikelyDead = sinceHeartbeat > DEAD_HEARTBEAT_THRESHOLD;
|
|
74
75
|
let networks = networksPerMachine.get(machineId) || [];
|
|
@@ -86,10 +87,13 @@ export class MachinePicker extends qreact.Component<{
|
|
|
86
87
|
}
|
|
87
88
|
}}
|
|
88
89
|
>
|
|
89
|
-
<div className={css.fontSize(17).fontWeight("bold")}>
|
|
90
|
-
{ip || machineId}
|
|
90
|
+
<div className={css.fontSize(17).fontWeight("bold") + (!name && css.colorhsl(0, 0, 45))}>
|
|
91
|
+
{name || ip || machineId}
|
|
91
92
|
</div>
|
|
92
|
-
{ip && <div className={css.fontSize(
|
|
93
|
+
{name && ip && <div className={css.fontSize(12).colorhsl(0, 0, 30)}>
|
|
94
|
+
{ip}
|
|
95
|
+
</div>}
|
|
96
|
+
{(name || ip) && <div className={css.fontSize(11).colorhsl(0, 0, 40)}>
|
|
93
97
|
{machineId}
|
|
94
98
|
</div>}
|
|
95
99
|
{networks.length > 0 && <div className={css.fontSize(11).colorhsl(220, 60, 40)}>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { qreact } from "../../4-dom/qreact";
|
|
3
|
-
import { MACHINE_RESYNC_INTERVAL, MachineServiceController, getMachineInfoLive } from "../machineSchema";
|
|
3
|
+
import { MACHINE_RESYNC_INTERVAL, MachineServiceController, getMachineInfoLive, getMachineNameLive } from "../machineSchema";
|
|
4
4
|
import { css } from "typesafecss";
|
|
5
5
|
import { t } from "../../2-proxy/schema2";
|
|
6
6
|
import { Querysub } from "../../4-querysub/Querysub";
|
|
@@ -78,8 +78,11 @@ export class MachinesListPage extends qreact.Component {
|
|
|
78
78
|
onClick={() => {
|
|
79
79
|
if (!hasSelectedMachines) return;
|
|
80
80
|
|
|
81
|
-
const
|
|
82
|
-
|
|
81
|
+
const machineDescriptions = selectedMachineIds.map(id => {
|
|
82
|
+
let name = getMachineNameLive(id);
|
|
83
|
+
return name && `${name} (${id})` || id;
|
|
84
|
+
}).join(", ");
|
|
85
|
+
const confirmed = confirm(`⚠️ WARNING: This will permanently delete ${selectedMachineIds.length} machine(s):\n\n${machineDescriptions}\n\nThis will remove all machine data and cannot be undone. Are you sure?`);
|
|
83
86
|
if (!confirmed) return;
|
|
84
87
|
|
|
85
88
|
Querysub.commit(() => {
|
|
@@ -125,6 +128,7 @@ export class MachinesListPage extends qreact.Component {
|
|
|
125
128
|
const isSelected = this.state.selectedForDeletion[machineId];
|
|
126
129
|
const machineConfig = machineConfigs?.find(x => x.machineId === machineId);
|
|
127
130
|
const isDisabled = machineConfig?.disabled || false;
|
|
131
|
+
const machineName = getMachineNameLive(machineId);
|
|
128
132
|
|
|
129
133
|
let failingServices = Object.keys(machineInfo.services).filter(serviceId => {
|
|
130
134
|
return machineInfo.services[serviceId].errorFromLastRun;
|
|
@@ -183,11 +187,14 @@ export class MachinesListPage extends qreact.Component {
|
|
|
183
187
|
<RenderGitRefInfo gitRef={machineInfo.gitRef} />
|
|
184
188
|
{Object.values(machineInfo.info).map(value => {
|
|
185
189
|
if (typeof value === "string") return undefined;
|
|
186
|
-
return <UsageBar label={value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
|
|
190
|
+
return <UsageBar label={value.label || value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
|
|
187
191
|
})}
|
|
188
192
|
</div>
|
|
189
193
|
<div className={css.vbox(4).flexGrow(1)}>
|
|
190
|
-
<div>
|
|
194
|
+
<div className={css.fontSize(22).fontWeight("bold") + (!machineName && css.colorhsl(0, 0, 45))}>
|
|
195
|
+
{machineName || "(unnamed machine)"}
|
|
196
|
+
</div>
|
|
197
|
+
<div className={css.fontSize(12).colorhsl(0, 0, 35)}>
|
|
191
198
|
{machineId}
|
|
192
199
|
</div>
|
|
193
200
|
<div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { qreact } from "../../4-dom/qreact";
|
|
3
|
-
import { DEFAULT_OVERLAP_TIME, MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, ServiceParameters, applyCommandTemplate, getCommandTemplateVariables, getLiveServiceParameters, getMachineInfoLive, getMachineTargets, setMachineTargets, stripReleaseState } from "../machineSchema";
|
|
3
|
+
import { DEFAULT_OVERLAP_TIME, MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, ServiceParameters, applyCommandTemplate, getCommandTemplateVariables, getLiveServiceParameters, getMachineInfoLive, getMachineNameLive, getMachineTargets, setMachineTargets, stripReleaseState } from "../machineSchema";
|
|
4
4
|
import { css } from "typesafecss";
|
|
5
5
|
import { t } from "../../2-proxy/schema2";
|
|
6
6
|
import { Querysub } from "../../4-querysub/Querysub";
|
|
@@ -126,10 +126,12 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
126
126
|
ip = "";
|
|
127
127
|
}
|
|
128
128
|
let networks = networksPerMachine.get(machineId) || [];
|
|
129
|
+
let name = getMachineNameLive(machineId);
|
|
129
130
|
return <div key={machineId} className={css.vbox(6).fillWidth.pad2(8, 6).hsl(0, 0, 94)}>
|
|
130
131
|
<div className={css.hbox(10).alignItems("baseline")}>
|
|
131
|
-
<div className={css.boldStyle.fontSize(
|
|
132
|
-
{ip && <div className={css.fontSize(
|
|
132
|
+
<div className={css.boldStyle.fontSize(20) + (!name && css.colorhsl(0, 0, 45))}>{name || ip || machineId}</div>
|
|
133
|
+
{name && ip && <div className={css.fontSize(13)}>{ip}</div>}
|
|
134
|
+
{(name || ip) && <div className={css.fontSize(11).colorhsl(0, 0, 40)}>{machineId}</div>}
|
|
133
135
|
{networks.length > 0 && <div className={css.fontSize(11).colorhsl(220, 60, 40)}>
|
|
134
136
|
{networks.join(", ")}
|
|
135
137
|
</div>}
|
|
@@ -237,6 +239,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
237
239
|
|
|
238
240
|
return {
|
|
239
241
|
machineId,
|
|
242
|
+
machineName: getMachineNameLive(machineId),
|
|
240
243
|
variables,
|
|
241
244
|
machineInfo,
|
|
242
245
|
serviceInfo,
|
|
@@ -355,7 +358,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
355
358
|
{/* The machines are notified on every config change and poll as a fallback, so forcing a resync had no use in practice. Kept in case that changes.
|
|
356
359
|
<ResyncMachinesButton applyNodeIds={machineStatuses.map(x => x.machineInfo?.applyNodeId || "")} /> */}
|
|
357
360
|
<div className={css.vbox(4).fillWidth}>
|
|
358
|
-
{machineStatuses.map(({ machineId, variables, machineInfo, serviceInfo, isMachineDead, hasError, isDisabled, index }) => {
|
|
361
|
+
{machineStatuses.map(({ machineId, machineName, variables, machineInfo, serviceInfo, isMachineDead, hasError, isDisabled, index }) => {
|
|
359
362
|
if (!machineInfo) return <div key={machineId}>Loading {machineId}...</div>;
|
|
360
363
|
|
|
361
364
|
let backgroundColor = css.hsl(0, 0, 100); // Default: white
|
|
@@ -378,7 +381,10 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
378
381
|
<div
|
|
379
382
|
className={css.hbox(12)}
|
|
380
383
|
>
|
|
381
|
-
<div className={css.hbox(5).wrap}>
|
|
384
|
+
<div className={css.hbox(5).wrap.alignItems("baseline")}>
|
|
385
|
+
<div className={css.fontSize(20).boldStyle + (!machineName && css.colorhsl(0, 0, 45))}>
|
|
386
|
+
{machineName || "(unnamed machine)"}
|
|
387
|
+
</div>
|
|
382
388
|
<div>
|
|
383
389
|
{screenName}
|
|
384
390
|
</div>
|
|
@@ -63,29 +63,46 @@ const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ memo
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
const
|
|
66
|
+
const LARGE_DISK_MIN_BYTES = 100 * 1024 * 1024 * 1024;
|
|
67
|
+
const DF_LINE_REGEX = /^(\S+)\s+(\d+)\s+(\d+)\s+\d+\s+\S+\s+(.+?)\s*$/;
|
|
68
|
+
|
|
69
|
+
type DiskInfo = { device: string; mountPoint: string; value: number; max: number };
|
|
70
|
+
|
|
71
|
+
const getDiskInfos = measureWrap(async function getDiskInfos(): Promise<DiskInfo[]> {
|
|
67
72
|
if (os.platform() === "win32") {
|
|
68
73
|
throw new Error("Windows is not supported for machine resource monitoring");
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
try {
|
|
72
|
-
|
|
73
|
-
let
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
let result = await runPromise("df -B1 -P", { quiet: true });
|
|
78
|
+
let disks: DiskInfo[] = [];
|
|
79
|
+
for (let line of result.split("\n").slice(1)) {
|
|
80
|
+
let match = DF_LINE_REGEX.exec(line);
|
|
81
|
+
if (!match) continue;
|
|
82
|
+
let [, device, totalText, usedText, mountPoint] = match;
|
|
83
|
+
if (!device.startsWith("/dev/") || device.startsWith("/dev/loop")) continue;
|
|
84
|
+
let max = parseInt(totalText);
|
|
85
|
+
let value = parseInt(usedText);
|
|
86
|
+
if (!max || !(value >= 0)) continue;
|
|
87
|
+
let existing = disks.find(disk => disk.device === device);
|
|
88
|
+
if (existing) {
|
|
89
|
+
if (mountPoint.length < existing.mountPoint.length) {
|
|
90
|
+
existing.mountPoint = mountPoint;
|
|
82
91
|
}
|
|
92
|
+
continue;
|
|
83
93
|
}
|
|
94
|
+
disks.push({ device, mountPoint, value, max });
|
|
84
95
|
}
|
|
96
|
+
let large = disks.filter(disk => disk.max >= LARGE_DISK_MIN_BYTES);
|
|
97
|
+
if (large.length > 0) {
|
|
98
|
+
disks = large;
|
|
99
|
+
}
|
|
100
|
+
sort(disks, disk => -disk.max);
|
|
101
|
+
return disks;
|
|
85
102
|
} catch (e: any) {
|
|
86
|
-
console.warn(`Error getting disk info: ${e.
|
|
103
|
+
console.warn(`Error getting disk info: ${e.stack}`);
|
|
87
104
|
}
|
|
88
|
-
return
|
|
105
|
+
return [];
|
|
89
106
|
});
|
|
90
107
|
|
|
91
108
|
const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
|
|
@@ -100,9 +117,9 @@ const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
|
|
|
100
117
|
};
|
|
101
118
|
|
|
102
119
|
// Get system resource information
|
|
103
|
-
let [memoryInfo,
|
|
120
|
+
let [memoryInfo, diskInfos] = await Promise.all([
|
|
104
121
|
getMemoryInfo(),
|
|
105
|
-
|
|
122
|
+
getDiskInfos()
|
|
106
123
|
]);
|
|
107
124
|
|
|
108
125
|
if (memoryInfo?.memory) {
|
|
@@ -121,9 +138,10 @@ const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
|
|
|
121
138
|
};
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
machineInfo.info
|
|
141
|
+
for (let diskInfo of diskInfos) {
|
|
142
|
+
machineInfo.info[`disk ${diskInfo.mountPoint}`] = {
|
|
126
143
|
type: "DISK",
|
|
144
|
+
label: `DISK ${diskInfo.mountPoint} (${diskInfo.device})`,
|
|
127
145
|
value: diskInfo.value,
|
|
128
146
|
max: diskInfo.max,
|
|
129
147
|
};
|
|
@@ -38,6 +38,12 @@ export type MachineConfig = {
|
|
|
38
38
|
export const machineInfos = archiveJSONT<MachineInfo>(() => getArchives2("machines/machine-heartbeats/"), { setFallbacks: true, findFallbacks: true });
|
|
39
39
|
export const serviceConfigs = archiveJSONT<ServiceConfig>(() => getArchives2("machines/service-configs/"), { setFallbacks: true, findFallbacks: true });
|
|
40
40
|
export const machineConfigs = archiveJSONT<MachineConfig>(() => getArchives2("machines/machine-configs/"), { setFallbacks: true, findFallbacks: true });
|
|
41
|
+
export const machineNames = archiveJSONT<MachineName>(() => getArchives2("machines/machine-names/"), { setFallbacks: true, findFallbacks: true });
|
|
42
|
+
|
|
43
|
+
export type MachineName = {
|
|
44
|
+
machineId: string;
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
|
41
47
|
|
|
42
48
|
export type MachineInfo = {
|
|
43
49
|
machineId: string;
|
|
@@ -57,6 +63,7 @@ export type MachineInfo = {
|
|
|
57
63
|
type: string;
|
|
58
64
|
value: number;
|
|
59
65
|
max: number;
|
|
66
|
+
label?: string;
|
|
60
67
|
}>;
|
|
61
68
|
|
|
62
69
|
repoUrl: string;
|
|
@@ -292,7 +299,19 @@ export class MachineServiceControllerBase {
|
|
|
292
299
|
public async deleteMachineIds(machineIds: string[]) {
|
|
293
300
|
for (let machineId of machineIds) {
|
|
294
301
|
await machineInfos.delete(machineId);
|
|
302
|
+
await machineNames.delete(machineId);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
public async getMachineNameList(): Promise<MachineName[]> {
|
|
306
|
+
return await machineNames.values();
|
|
307
|
+
}
|
|
308
|
+
public async setMachineName(machineId: string, name: string) {
|
|
309
|
+
name = name.trim();
|
|
310
|
+
if (!name) {
|
|
311
|
+
await machineNames.delete(machineId);
|
|
312
|
+
return;
|
|
295
313
|
}
|
|
314
|
+
await machineNames.set(machineId, { machineId, name });
|
|
296
315
|
}
|
|
297
316
|
public async getMachineInfo(machineId: string): Promise<MachineInfo | undefined> {
|
|
298
317
|
return await machineInfos.get(machineId);
|
|
@@ -641,6 +660,8 @@ export const MachineServiceController = getSyncedController(
|
|
|
641
660
|
getMachineConfigList: {},
|
|
642
661
|
getMachineConfig: {},
|
|
643
662
|
setMachineConfig: {},
|
|
663
|
+
getMachineNameList: {},
|
|
664
|
+
setMachineName: {},
|
|
644
665
|
addServiceConfig: {},
|
|
645
666
|
setServiceConfigs: {},
|
|
646
667
|
getServiceConfigType: {},
|
|
@@ -661,7 +682,8 @@ export const MachineServiceController = getSyncedController(
|
|
|
661
682
|
),
|
|
662
683
|
{
|
|
663
684
|
writes: {
|
|
664
|
-
deleteMachineIds: ["MachineInfo", "MachineInfoList"],
|
|
685
|
+
deleteMachineIds: ["MachineInfo", "MachineInfoList", "MachineName"],
|
|
686
|
+
setMachineName: ["MachineName"],
|
|
665
687
|
addMachineInfo: ["MachineInfo", "MachineInfoList"],
|
|
666
688
|
setMachineInfo: ["MachineInfo"],
|
|
667
689
|
setMachineConfig: ["MachineConfig", "MachineConfigList"],
|
|
@@ -677,6 +699,7 @@ export const MachineServiceController = getSyncedController(
|
|
|
677
699
|
getMachineInfo: ["MachineInfo"],
|
|
678
700
|
getMachineConfigList: ["MachineConfigList"],
|
|
679
701
|
getMachineConfig: ["MachineConfig"],
|
|
702
|
+
getMachineNameList: ["MachineName"],
|
|
680
703
|
getServiceList: ["ServiceConfigList"],
|
|
681
704
|
getServiceConfig: ["ServiceConfig"],
|
|
682
705
|
getGitInfo: ["gitInfo"],
|
|
@@ -703,3 +726,14 @@ export function getMachineInfoLive(machineId: string): MachineInfo | undefined {
|
|
|
703
726
|
startMachineInfoRefreshLoop();
|
|
704
727
|
return MachineServiceController(SocketFunction.browserNodeId()).getMachineInfo(machineId);
|
|
705
728
|
}
|
|
729
|
+
|
|
730
|
+
export function getMachineNameLive(machineId: string): string {
|
|
731
|
+
let names = MachineServiceController(SocketFunction.browserNodeId()).getMachineNameList();
|
|
732
|
+
return names?.find(x => x.machineId === machineId)?.name || "";
|
|
733
|
+
}
|
|
734
|
+
export function setMachineNameLive(machineId: string, name: string) {
|
|
735
|
+
let controller = MachineServiceController(SocketFunction.browserNodeId());
|
|
736
|
+
Querysub.onCommitFinished(async () => {
|
|
737
|
+
await controller.setMachineName.promise(machineId, name);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
@@ -7,6 +7,11 @@ import { fsExistsAsync } from "../fs";
|
|
|
7
7
|
import { delay } from "socket-function/src/batching";
|
|
8
8
|
import { SERVICE_NAME, SERVICE_UNIT_NAME } from "./machineDaemonShared";
|
|
9
9
|
import { addDeployKeyToGitHub, askQuestion } from "./githubRepoAccess";
|
|
10
|
+
import { getOrCreateRemoteMachineId } from "sliftutils/security/machines/machines";
|
|
11
|
+
import { readRemoteFile, runOverSSH, SUDO_PREAMBLE } from "sliftutils/security/helpers/remoteSSH";
|
|
12
|
+
import { DEV_getIdentityFilePath, getMachineId, IdentityStorageType } from "sliftutils/misc/https/certs";
|
|
13
|
+
import { trustMachineCertificate } from "../-d-trust/NetworkTrust2";
|
|
14
|
+
import { getDomain } from "../config";
|
|
10
15
|
// Import querysub, to fix missing dependencies
|
|
11
16
|
Querysub;
|
|
12
17
|
|
|
@@ -142,6 +147,34 @@ async function installUnofficialNode(sshRemote: string, major: number): Promise<
|
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
149
|
|
|
150
|
+
async function setupMachineIdentity(sshRemote: string): Promise<string> {
|
|
151
|
+
let domain = getDomain();
|
|
152
|
+
let machineId = await getOrCreateRemoteMachineId(sshRemote, domain);
|
|
153
|
+
let home = await sshValue(sshRemote, "echo $HOME");
|
|
154
|
+
let identityPath = `${home}/${path.basename(DEV_getIdentityFilePath(domain))}`;
|
|
155
|
+
await runOverSSH({
|
|
156
|
+
host: sshRemote,
|
|
157
|
+
script: `${SUDO_PREAMBLE}\n$SUDO chown "$(id -u):$(id -g)" "${identityPath}"`,
|
|
158
|
+
allowFailure: true,
|
|
159
|
+
});
|
|
160
|
+
let contents = await readRemoteFile({ host: sshRemote, filePath: identityPath });
|
|
161
|
+
if (!contents) {
|
|
162
|
+
throw new Error(`Expected an identity at ${identityPath} on ${sshRemote} after creating one, there is none`);
|
|
163
|
+
}
|
|
164
|
+
let stored = JSON.parse(contents) as IdentityStorageType;
|
|
165
|
+
let storedMachineId = getMachineId(stored.domain, domain);
|
|
166
|
+
if (storedMachineId !== machineId) {
|
|
167
|
+
throw new Error(`Expected ${identityPath} on ${sshRemote} to hold ${machineId}, it holds ${storedMachineId}. Something else wrote an identity at the same time, so re-run setup.`);
|
|
168
|
+
}
|
|
169
|
+
let published = await trustMachineCertificate({ machineId, cert: Buffer.from(stored.certB64, "base64") });
|
|
170
|
+
if (published) {
|
|
171
|
+
console.log(`✅ Machine ${machineId} identified and added to the trust archive`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(`✅ Machine ${machineId} identified, and was already in the trust archive`);
|
|
174
|
+
}
|
|
175
|
+
return machineId;
|
|
176
|
+
}
|
|
177
|
+
|
|
145
178
|
async function setupRepositoryOnRemote(sshRemote: string, gitURLLive: string, gitRefLive: string): Promise<void> {
|
|
146
179
|
// Create git folder on remote
|
|
147
180
|
await runPromise(`ssh ${sshRemote} "mkdir -p ~/machine-alwaysup"`);
|
|
@@ -282,6 +315,9 @@ async function main() {
|
|
|
282
315
|
console.warn("⚠️ Backblaze file not found at:", backblazePath);
|
|
283
316
|
}
|
|
284
317
|
|
|
318
|
+
console.log("Setting up the machine's identity and trust...");
|
|
319
|
+
let machineId = await setupMachineIdentity(sshRemote);
|
|
320
|
+
|
|
285
321
|
// 2. Ensure git is installed on remote server
|
|
286
322
|
console.log("Ensuring git is installed...");
|
|
287
323
|
try {
|
|
@@ -293,15 +329,31 @@ async function main() {
|
|
|
293
329
|
console.log("✅ Git installed");
|
|
294
330
|
}
|
|
295
331
|
|
|
296
|
-
// 3. Ensure build tools are installed (needed for native modules)
|
|
297
332
|
console.log("Ensuring build tools are installed...");
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
333
|
+
let buildTools = ["make", "g++", "cc"];
|
|
334
|
+
let missingBuildTools: string[] = [];
|
|
335
|
+
for (let tool of buildTools) {
|
|
336
|
+
let found = await runPromise(`ssh ${sshRemote} "command -v ${tool} || true"`, { nothrow: true });
|
|
337
|
+
if (!found.trim()) {
|
|
338
|
+
missingBuildTools.push(tool);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (missingBuildTools.length === 0) {
|
|
342
|
+
console.log(`✅ Build tools already installed (${buildTools.join(", ")})`);
|
|
343
|
+
} else {
|
|
344
|
+
console.log(`Installing build tools (missing: ${missingBuildTools.join(", ")})...`);
|
|
303
345
|
await runPromise(`ssh ${sshRemote} "sudo apt install -y build-essential"`);
|
|
304
|
-
|
|
346
|
+
let stillMissing: string[] = [];
|
|
347
|
+
for (let tool of missingBuildTools) {
|
|
348
|
+
let found = await runPromise(`ssh ${sshRemote} "command -v ${tool} || true"`, { nothrow: true });
|
|
349
|
+
if (!found.trim()) {
|
|
350
|
+
stillMissing.push(tool);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (stillMissing.length > 0) {
|
|
354
|
+
throw new Error(`Installed build-essential on ${sshRemote}, but ${stillMissing.join(", ")} is still not on the PATH. Native modules (fs-ext, and everything else node-gyp builds) cannot compile without it - check what "sudo apt install -y build-essential" reports on that machine.`);
|
|
355
|
+
}
|
|
356
|
+
console.log(`✅ Build tools installed (${buildTools.join(", ")})`);
|
|
305
357
|
}
|
|
306
358
|
|
|
307
359
|
// The daemon runs everything in tmux screens, so setup cannot get by without it
|
|
@@ -488,6 +540,7 @@ sudo systemctl start ${SERVICE_UNIT_NAME}
|
|
|
488
540
|
}
|
|
489
541
|
|
|
490
542
|
console.log("✅ Machine service started!");
|
|
543
|
+
console.log(` Machine: ${machineId}`);
|
|
491
544
|
console.log(` Screen: ssh ${sshRemote} -t "tmux attach -t ${SERVICE_NAME}"`);
|
|
492
545
|
console.log(` Status: ssh ${sshRemote} "systemctl status ${SERVICE_UNIT_NAME}"`);
|
|
493
546
|
|
|
@@ -45,6 +45,10 @@ export class MachineThreadInfo extends qreact.Component<{
|
|
|
45
45
|
handleAttach = async (threadInfo: NodeSpecialInfo | undefined) => {
|
|
46
46
|
Querysub.onCommitFinished(async () => {
|
|
47
47
|
const url = await NodeViewerController.nodes[SocketFunction.getBrowserNodeId()].getExternalInspectURL(threadInfo?.nodeId || "");
|
|
48
|
+
if (!url) {
|
|
49
|
+
console.log(`No assisted inspect URL for ${threadInfo?.nodeId}, connect to the debugger directly.`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
48
52
|
window.open(url, "_blank");
|
|
49
53
|
});
|
|
50
54
|
};
|
|
@@ -5,7 +5,7 @@ import { getAllNodeIds, getBrowserUrlNode, syncNodesNow } from "../../src/-f-nod
|
|
|
5
5
|
import { errorToUndefined, errorToUndefinedSilent, logErrors, timeoutToError } from "../../src/errors";
|
|
6
6
|
import { QuerysubController } from "../../src/4-querysub/QuerysubController";
|
|
7
7
|
import { Querysub } from "../../src/4-querysub/Querysub";
|
|
8
|
-
import { NodeCapabilitiesController,
|
|
8
|
+
import { NodeCapabilitiesController, isAssistedDebugConnectionAllowed, getControllerNodeIdList } from "../../src/-g-core-values/NodeCapabilities";
|
|
9
9
|
import { lazy } from "socket-function/src/caching";
|
|
10
10
|
import { atomicObjectWrite } from "../../src/2-proxy/PathValueProxyWatcher";
|
|
11
11
|
import { Button } from "../../src/library-components/Button";
|
|
@@ -274,6 +274,10 @@ export class NodeViewer extends qreact.Component {
|
|
|
274
274
|
<button onClick={async () => {
|
|
275
275
|
const controller = NodeViewerController.nodes[getBrowserUrlNode()];
|
|
276
276
|
let url = await controller.getExternalInspectURL(str);
|
|
277
|
+
if (!url) {
|
|
278
|
+
console.log(`No assisted inspect URL for ${str}, connect to the debugger directly.`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
277
281
|
console.log(url);
|
|
278
282
|
console.log(url.replace("https://notdevtools.com/devtools", "devtools://devtools/bundled"));
|
|
279
283
|
// devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&wss=99-250-124-91.querysub.com:2058/cc5b76b2-549f-4a66-8026-021811e9edd3
|
|
@@ -354,8 +358,8 @@ class NodeViewerControllerBase {
|
|
|
354
358
|
return (await dns.promises.lookup(callerIP)).address;
|
|
355
359
|
}
|
|
356
360
|
|
|
357
|
-
public async getExternalInspectURL(nodeId: string) {
|
|
358
|
-
|
|
361
|
+
public async getExternalInspectURL(nodeId: string): Promise<string | undefined> {
|
|
362
|
+
if (!isAssistedDebugConnectionAllowed()) return undefined;
|
|
359
363
|
// TODO: Replace this with a an HTTPS => HTTP proxy, adding a secret time based key
|
|
360
364
|
// - Needs to run in another process
|
|
361
365
|
// - Key will use machine hash(secret key + time mod interval). Will last for 2X interval, so at
|
|
@@ -365,7 +369,9 @@ class NodeViewerControllerBase {
|
|
|
365
369
|
|
|
366
370
|
let callerIP = getNodeIdIP(SocketFunction.getCaller().nodeId);
|
|
367
371
|
let ourIP = await getExternalIP();
|
|
368
|
-
let
|
|
372
|
+
let exposed = await NodeCapabilitiesController.nodes[nodeId].exposeExternalDebugPortOnce(ourIP);
|
|
373
|
+
if (!exposed) return undefined;
|
|
374
|
+
let { externalPort, internalPort, internalInspectURL } = exposed;
|
|
369
375
|
|
|
370
376
|
const forwardCode = ((config: {
|
|
371
377
|
callerIP: string;
|
|
@@ -482,8 +488,8 @@ class NodeViewerControllerBase {
|
|
|
482
488
|
return internalInspectURL.replace(`ws=127.0.0.1:${internalPort}`, `wss=${ourDomain}:${finalPort}`);
|
|
483
489
|
}
|
|
484
490
|
|
|
485
|
-
public async getInspectURL(nodeId: string) {
|
|
486
|
-
|
|
491
|
+
public async getInspectURL(nodeId: string): Promise<string | undefined> {
|
|
492
|
+
if (!isAssistedDebugConnectionAllowed()) return undefined;
|
|
487
493
|
return await NodeCapabilitiesController.nodes[nodeId].getInspectURL();
|
|
488
494
|
}
|
|
489
495
|
|
|
@@ -297,6 +297,9 @@ async function attachToNode(nodeId: string): Promise<AttachNodeResult> {
|
|
|
297
297
|
// Same machine — the inspector is reachable directly on localhost.
|
|
298
298
|
mode = "local";
|
|
299
299
|
const inspectUrl = await NodeCapabilitiesController.nodes[nodeId].getInspectURL();
|
|
300
|
+
if (!inspectUrl) {
|
|
301
|
+
throw new Error(`Node ${nodeId} is not public, so it has no assisted inspect URL. Connect to its debugger directly.`);
|
|
302
|
+
}
|
|
300
303
|
const { host, port, uuidPath } = parseInspectWs(inspectUrl);
|
|
301
304
|
wsUrl = `ws://${host}:${port}${uuidPath}`;
|
|
302
305
|
detail =
|
|
@@ -306,8 +309,11 @@ async function attachToNode(nodeId: string): Promise<AttachNodeResult> {
|
|
|
306
309
|
// Remote machine — have the node forward its inspector port to a
|
|
307
310
|
// one-time external port locked to our IP.
|
|
308
311
|
mode = "remote";
|
|
309
|
-
const
|
|
310
|
-
|
|
312
|
+
const exposed = await NodeCapabilitiesController.nodes[nodeId].exposeExternalDebugPortOnce(ourIP);
|
|
313
|
+
if (!exposed) {
|
|
314
|
+
throw new Error(`Node ${nodeId} is not public, so it cannot expose an external debug port. Connect to its debugger directly.`);
|
|
315
|
+
}
|
|
316
|
+
const { externalPort, internalPort, internalInspectURL } = exposed;
|
|
311
317
|
const { uuidPath } = parseInspectWs(internalInspectURL);
|
|
312
318
|
wsUrl = `ws://${nodeIP}:${externalPort}${uuidPath}`;
|
|
313
319
|
detail =
|