querysub 0.451.0 → 0.453.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 +12 -1
- package/bin/join-public.js +1 -0
- package/package.json +1 -1
- package/src/-a-archives/archiveCache.ts +53 -597
- package/src/-g-core-values/NodeCapabilities.ts +29 -28
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +24 -0
- package/src/0-path-value-core/pathValueCore.ts +1 -1
- package/src/2-proxy/PathValueProxyWatcher.ts +6 -6
- package/src/4-querysub/Querysub.ts +15 -13
- package/src/archiveapps/archiveGCEntry.tsx +1 -0
- package/src/archiveapps/archiveJoinEntry.ts +8 -2
- package/src/deployManager/LaunchTrackingHeader.tsx +65 -0
- package/src/deployManager/machineApplyMainCode.ts +140 -15
- package/src/deployManager/machineSchema.ts +82 -1
- package/src/diagnostics/NodeConnectionsPage.tsx +1 -1
- package/src/diagnostics/NodeViewer.tsx +15 -25
- package/src/diagnostics/debugger/mcp-server.ts +327 -53
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +64 -22
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +32 -1
- package/src/diagnostics/managementPages.tsx +8 -0
- package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +113 -0
- package/src/diagnostics/pathAuditer.ts +0 -6
- package/test.ts +2 -1
- package/src/misc/getParentProcessId.cs +0 -53
- package/src/misc/getParentProcessId.ts +0 -53
|
@@ -27,6 +27,32 @@ const PROGRESS_LOG_INTERVAL = timeInSecond * 5;
|
|
|
27
27
|
const LOGGER_NAMES = ["logs/log", "logs/info", "logs/warn", "logs/error"] as const;
|
|
28
28
|
type LoggerName = typeof LOGGER_NAMES[number];
|
|
29
29
|
|
|
30
|
+
// Public-facing short names callers pass in via the `logTypes` parameter, mapped
|
|
31
|
+
// to the internal logger names above.
|
|
32
|
+
const EXTERNAL_TO_INTERNAL_LOGGER: Record<string, LoggerName> = {
|
|
33
|
+
"log": "logs/log",
|
|
34
|
+
"info": "logs/info",
|
|
35
|
+
"warn": "logs/warn",
|
|
36
|
+
"error": "logs/error",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Parses the caller's `logTypes` string (e.g. "warn|error") into the matching
|
|
40
|
+
// internal logger names. Empty / undefined means "all four" (no restriction).
|
|
41
|
+
function parseLogTypes(value: string | undefined): readonly LoggerName[] {
|
|
42
|
+
if (!value) return LOGGER_NAMES;
|
|
43
|
+
let parts = value.split("|").map(s => s.trim().toLowerCase()).filter(s => s);
|
|
44
|
+
if (parts.length === 0) return LOGGER_NAMES;
|
|
45
|
+
let result: LoggerName[] = [];
|
|
46
|
+
for (let p of parts) {
|
|
47
|
+
let internal = EXTERNAL_TO_INTERNAL_LOGGER[p];
|
|
48
|
+
if (!internal) {
|
|
49
|
+
throw new Error(`logTypes: unknown log type ${JSON.stringify(p)}; expected one of log, info, warn, error (separated by "|")`);
|
|
50
|
+
}
|
|
51
|
+
if (!result.includes(internal)) result.push(internal);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
// Per-logger accounting for one search. Byte counts are raw buffer sizes.
|
|
31
57
|
export type LoggerStats = {
|
|
32
58
|
// Files in range matching the requested machine.
|
|
@@ -142,8 +168,11 @@ function createProgressLogger(): (message: string) => void {
|
|
|
142
168
|
}
|
|
143
169
|
|
|
144
170
|
export class MCPIndexedLogs {
|
|
145
|
-
// machineId -> latest timestamp guaranteed to already be
|
|
146
|
-
|
|
171
|
+
// `${machineId}|${loggerName}` -> latest timestamp guaranteed to already be
|
|
172
|
+
// moved-to-public for that specific (machine, logger). Keyed per-logger so
|
|
173
|
+
// a search scoped to only some loggers doesn't poison the cache for the
|
|
174
|
+
// others.
|
|
175
|
+
private movedThroughByMachineLogger = new Map<string, number>();
|
|
147
176
|
|
|
148
177
|
// Cache: `${type}|${loggerName}|${startBucket}|${endBucket}` -> { time, paths }.
|
|
149
178
|
// Buckets are hour-aligned start/end so adjacent searches reuse work.
|
|
@@ -157,11 +186,15 @@ export class MCPIndexedLogs {
|
|
|
157
186
|
direction: Direction;
|
|
158
187
|
columns: string[];
|
|
159
188
|
limit?: number;
|
|
189
|
+
// Optional pipe-separated list restricting which log streams to scan
|
|
190
|
+
// (e.g. "warn|error", "log"). Omit / empty = all four streams.
|
|
191
|
+
logTypes?: string;
|
|
160
192
|
}): Promise<SearchResult> {
|
|
161
193
|
let limit = config.limit ?? 100;
|
|
162
194
|
let startTime = normalizeTime(config.startTime, "startTime");
|
|
163
195
|
let endTime = normalizeTime(config.endTime, "endTime");
|
|
164
|
-
|
|
196
|
+
let enabledLoggers = parseLogTypes(config.logTypes);
|
|
197
|
+
console.log(`[search] query=${JSON.stringify(config.query)} | machine=${config.machine} | startTime=${formatDateTime(startTime)} | endTime=${formatDateTime(endTime)} | direction=${config.direction} | columns=[${config.columns.join(",")}] | limit=${config.limit ?? "(default)"} | logTypes=${config.logTypes ?? "(all)"}`);
|
|
165
198
|
let now = Date.now();
|
|
166
199
|
if (endTime > now - END_TIME_MIN_AGE) {
|
|
167
200
|
throw new Error(`endTime must be at least ${formatTime(END_TIME_MIN_AGE)} in the past (got ${formatTime(now - endTime)} ago)`);
|
|
@@ -173,7 +206,7 @@ export class MCPIndexedLogs {
|
|
|
173
206
|
let machineId = config.machine === "local" ? getOwnMachineId() : config.machine;
|
|
174
207
|
|
|
175
208
|
let moveStart = Date.now();
|
|
176
|
-
let moveOutcome = await this.ensureMovedThrough(machineId, endTime);
|
|
209
|
+
let moveOutcome = await this.ensureMovedThrough(machineId, endTime, enabledLoggers);
|
|
177
210
|
console.log(`[search] ensureMovedThrough ${moveOutcome} in ${formatTime(Date.now() - moveStart)}`);
|
|
178
211
|
|
|
179
212
|
let loggers = await getLoggers2Async();
|
|
@@ -190,7 +223,7 @@ export class MCPIndexedLogs {
|
|
|
190
223
|
|
|
191
224
|
let pathsStart = Date.now();
|
|
192
225
|
let totalPathsSeen = 0;
|
|
193
|
-
await Promise.all(
|
|
226
|
+
await Promise.all(enabledLoggers.map(async (loggerName) => {
|
|
194
227
|
let logger = this.getLoggerByName(loggers, loggerName);
|
|
195
228
|
let archives = logger.debugGetCachedLogs({ type: useType });
|
|
196
229
|
|
|
@@ -300,9 +333,11 @@ export class MCPIndexedLogs {
|
|
|
300
333
|
|
|
301
334
|
// Trim the internal LoggerStats down to just total + scanned. The rest
|
|
302
335
|
// (bytes/blocks/timing) stays in the console.log above and is NOT
|
|
303
|
-
// returned — see the warning on SearchResult.
|
|
336
|
+
// returned — see the warning on SearchResult. We only emit entries for
|
|
337
|
+
// the loggers we actually searched, so a caller who scoped to
|
|
338
|
+
// `warn|error` doesn't see misleading 0s for the loggers they skipped.
|
|
304
339
|
let files: Record<string, { total: number; scanned: number }> = {};
|
|
305
|
-
for (let name of
|
|
340
|
+
for (let name of enabledLoggers) {
|
|
306
341
|
files[name] = { total: fileCounts[name].total, scanned: fileCounts[name].scanned };
|
|
307
342
|
}
|
|
308
343
|
|
|
@@ -450,16 +485,20 @@ export class MCPIndexedLogs {
|
|
|
450
485
|
sink.loggerStats.rows++;
|
|
451
486
|
}
|
|
452
487
|
|
|
453
|
-
// For each logger, asks each remote node on the target machine
|
|
454
|
-
// has pending logs overlapping [0, endTime]. The first node that
|
|
455
|
-
// without throwing wins; if it says yes, we ask the same node to
|
|
456
|
-
// We iterate because not every node necessarily exposes the new
|
|
457
|
-
// (e.g. older versions still running). Records moved-through up
|
|
458
|
-
// now - MOVE_GRACE so we skip this on subsequent
|
|
459
|
-
// window.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
488
|
+
// For each requested logger, asks each remote node on the target machine
|
|
489
|
+
// whether it has pending logs overlapping [0, endTime]. The first node that
|
|
490
|
+
// answers without throwing wins; if it says yes, we ask the same node to
|
|
491
|
+
// flush. We iterate because not every node necessarily exposes the new
|
|
492
|
+
// endpoints (e.g. older versions still running). Records moved-through up
|
|
493
|
+
// to now - MOVE_GRACE per (machine, logger) so we skip this on subsequent
|
|
494
|
+
// calls covering the same window. Only the loggers listed in `loggers` are
|
|
495
|
+
// touched; the others aren't queried or flushed.
|
|
496
|
+
private async ensureMovedThrough(machineId: string, endTime: number, loggers: readonly LoggerName[]): Promise<"cached" | "no-node" | "moved"> {
|
|
497
|
+
let needed = loggers.filter(name => {
|
|
498
|
+
let lastMoved = this.movedThroughByMachineLogger.get(`${machineId}|${name}`) ?? 0;
|
|
499
|
+
return lastMoved < endTime;
|
|
500
|
+
});
|
|
501
|
+
if (needed.length === 0) return "cached";
|
|
463
502
|
|
|
464
503
|
let nodeIds = await this.findRemoteNodesOnMachine(machineId);
|
|
465
504
|
if (nodeIds.length === 0) {
|
|
@@ -467,7 +506,7 @@ export class MCPIndexedLogs {
|
|
|
467
506
|
return "no-node";
|
|
468
507
|
}
|
|
469
508
|
|
|
470
|
-
for (let loggerName of
|
|
509
|
+
for (let loggerName of needed) {
|
|
471
510
|
let answered = false;
|
|
472
511
|
for (let nodeId of nodeIds) {
|
|
473
512
|
try {
|
|
@@ -495,7 +534,10 @@ export class MCPIndexedLogs {
|
|
|
495
534
|
}
|
|
496
535
|
}
|
|
497
536
|
|
|
498
|
-
|
|
537
|
+
let recordTime = Date.now() - MOVE_GRACE;
|
|
538
|
+
for (let loggerName of needed) {
|
|
539
|
+
this.movedThroughByMachineLogger.set(`${machineId}|${loggerName}`, recordTime);
|
|
540
|
+
}
|
|
499
541
|
return "moved";
|
|
500
542
|
}
|
|
501
543
|
|
|
@@ -511,11 +553,11 @@ export class MCPIndexedLogs {
|
|
|
511
553
|
let preferred: string[] = [];
|
|
512
554
|
let rest: string[] = [];
|
|
513
555
|
await Promise.all(candidates.map(async (nodeId) => {
|
|
514
|
-
let
|
|
556
|
+
let metadata = await timeoutToUndefinedSilent(
|
|
515
557
|
2500,
|
|
516
|
-
NodeCapabilitiesController.nodes[nodeId].
|
|
558
|
+
NodeCapabilitiesController.nodes[nodeId].getMetadata(),
|
|
517
559
|
);
|
|
518
|
-
if (
|
|
560
|
+
if (metadata?.entryPoint.includes("movelogs")) preferred.push(nodeId);
|
|
519
561
|
else rest.push(nodeId);
|
|
520
562
|
}));
|
|
521
563
|
return [...preferred, ...rest];
|
|
@@ -14,12 +14,15 @@ module.hotreload = false;
|
|
|
14
14
|
import "../../../inject";
|
|
15
15
|
|
|
16
16
|
import * as http from "http";
|
|
17
|
-
import { logErrors } from "../../../errors";
|
|
17
|
+
import { logErrors, timeoutToUndefinedSilent } from "../../../errors";
|
|
18
18
|
import { Querysub } from "../../../4-querysub/QuerysubController";
|
|
19
19
|
import { MCPIndexedLogs } from "./MCPIndexedLogs";
|
|
20
|
+
import { getAllNodeIds } from "../../../-f-node-discovery/NodeDiscovery";
|
|
21
|
+
import { NodeCapabilitiesController } from "../../../-g-core-values/NodeCapabilities";
|
|
20
22
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
21
23
|
|
|
22
24
|
const DEFAULT_MCP_HTTP_PORT = 4487;
|
|
25
|
+
const NODE_INFO_TIMEOUT_MS = 5000;
|
|
23
26
|
|
|
24
27
|
const PROTOCOL_VERSION = "2025-03-26";
|
|
25
28
|
const SERVER_INFO = { name: "querysub-indexed-logs", version: "0.1.0" };
|
|
@@ -33,6 +36,8 @@ Returns { allColumns, results, files: { <loggerName>: {total, scanned} } }. If a
|
|
|
33
36
|
|
|
34
37
|
\`allColumns\` is the union of every key seen on any returned row, so callers can pick additional columns for follow-up calls.
|
|
35
38
|
|
|
39
|
+
Optional \`logTypes\` parameter restricts which log streams are scanned. Pass a pipe-separated list of \`log\`, \`info\`, \`warn\`, \`error\` (e.g. \`"warn|error"\`, \`"log"\`). Omit to search all four. The returned \`files\` object only includes the streams you asked for.
|
|
40
|
+
|
|
36
41
|
Query syntax (case-insensitive substring match by default):
|
|
37
42
|
| — OR. \`cat|dog\` matches if either substring is present.
|
|
38
43
|
& — AND. \`error&timeout\` matches if both are present (in any order, anywhere).
|
|
@@ -50,10 +55,19 @@ Note: each segment between operators ideally has at least 4 contiguous character
|
|
|
50
55
|
direction: { type: "string", enum: ["fromStart", "fromEnd"] },
|
|
51
56
|
columns: { type: "array", items: { type: "string" }, description: "Which fields to project onto each row. Use [] to get just metadata; use allColumns from a prior result to pick more." },
|
|
52
57
|
limit: { type: "number", default: 100 },
|
|
58
|
+
logTypes: { type: "string", description: "Optional pipe-separated list restricting which log streams to scan. Allowed values: log, info, warn, error. Examples: \"warn|error\", \"log\". Omit (default) to search all four." },
|
|
53
59
|
},
|
|
54
60
|
required: ["query", "machine", "startTime", "endTime", "direction", "columns"],
|
|
55
61
|
},
|
|
56
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: "listNodes",
|
|
65
|
+
description: `List every node in the Querysub cluster, with each node's process entry point. Returns an array of { nodeId, entryPoint }. A node that does not answer in time has entryPoint left undefined.`,
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
57
71
|
];
|
|
58
72
|
|
|
59
73
|
type JsonRpcRequest = {
|
|
@@ -122,6 +136,8 @@ async function dispatch(method: string, params: unknown, mcp: MCPIndexedLogs): P
|
|
|
122
136
|
let result: unknown;
|
|
123
137
|
if (toolName === "search") {
|
|
124
138
|
result = await mcp.search(args as Parameters<MCPIndexedLogs["search"]>[0]);
|
|
139
|
+
} else if (toolName === "listNodes") {
|
|
140
|
+
result = await getNodeInfos();
|
|
125
141
|
} else {
|
|
126
142
|
throw new Error(`Unknown tool ${toolName}`);
|
|
127
143
|
}
|
|
@@ -143,6 +159,21 @@ function errorResponse(id: number | string | null, code: number, message: string
|
|
|
143
159
|
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
144
160
|
}
|
|
145
161
|
|
|
162
|
+
type NodeInfo = {
|
|
163
|
+
nodeId: string;
|
|
164
|
+
entryPoint?: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
async function getNodeInfos(): Promise<NodeInfo[]> {
|
|
168
|
+
let nodes = await getAllNodeIds();
|
|
169
|
+
return Promise.all(
|
|
170
|
+
nodes.map(async (nodeId): Promise<NodeInfo> => {
|
|
171
|
+
let metadata = await timeoutToUndefinedSilent(NODE_INFO_TIMEOUT_MS, NodeCapabilitiesController.nodes[nodeId].getMetadata());
|
|
172
|
+
return { nodeId, entryPoint: metadata?.entryPoint };
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
146
177
|
async function main() {
|
|
147
178
|
let mcp = new MCPIndexedLogs();
|
|
148
179
|
|
|
@@ -93,6 +93,12 @@ export async function registerManagementPages2(config: {
|
|
|
93
93
|
controllerName: "NodeViewerController",
|
|
94
94
|
getModule: () => import("./NodeViewer"),
|
|
95
95
|
});
|
|
96
|
+
inputPages.push({
|
|
97
|
+
title: "Authority Specs",
|
|
98
|
+
componentName: "AuthoritySpecPage",
|
|
99
|
+
controllerName: "AuthoritySpecPageController",
|
|
100
|
+
getModule: () => import("./misc-pages/AuthoritySpecPage"),
|
|
101
|
+
});
|
|
96
102
|
inputPages.push({
|
|
97
103
|
title: "LOG VIEWER",
|
|
98
104
|
componentName: "LogViewer3",
|
|
@@ -306,6 +312,7 @@ export function renderIsManagementUser() {
|
|
|
306
312
|
}
|
|
307
313
|
|
|
308
314
|
const ErrorWarning = createLazyComponent(() => import("./logs/errorNotifications2/ErrorWarning"))("ErrorWarning");
|
|
315
|
+
const LaunchTrackingHeader = createLazyComponent(() => import("../deployManager/LaunchTrackingHeader"))("LaunchTrackingHeader");
|
|
309
316
|
|
|
310
317
|
class ManagementRoot extends qreact.Component {
|
|
311
318
|
state = {
|
|
@@ -378,6 +385,7 @@ class ManagementRoot extends qreact.Component {
|
|
|
378
385
|
{isCurrentUserSuperUser() && <div className={css.vbox(4)}>
|
|
379
386
|
<PathDistributionInfo />
|
|
380
387
|
<ValuePathWarning />
|
|
388
|
+
<LaunchTrackingHeader />
|
|
381
389
|
</div>}
|
|
382
390
|
</div>
|
|
383
391
|
{currentPage &&
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module.allowclient = true;
|
|
2
|
+
|
|
3
|
+
import { qreact } from "../../4-dom/qreact";
|
|
4
|
+
import { css } from "typesafecss";
|
|
5
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
|
+
import { getAllNodeIds, getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
7
|
+
import { getSyncedController } from "../../library-components/SyncedController";
|
|
8
|
+
import { assertIsManagementUser } from "../managementPages";
|
|
9
|
+
import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilities";
|
|
10
|
+
import { timeoutToUndefinedSilent } from "../../errors";
|
|
11
|
+
import { sort } from "socket-function/src/misc";
|
|
12
|
+
import type { AuthoritySpec } from "../../0-path-value-core/PathRouter";
|
|
13
|
+
|
|
14
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
15
|
+
const RANGE_BAR_WIDTH_PX = 360;
|
|
16
|
+
const RANGE_BAR_HEIGHT_PX = 14;
|
|
17
|
+
|
|
18
|
+
type NodeAuthorityInfo = {
|
|
19
|
+
nodeId: string;
|
|
20
|
+
entryPoint?: string;
|
|
21
|
+
spec?: AuthoritySpec;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
class AuthoritySpecPageControllerBase {
|
|
25
|
+
public async getAllNodeAuthoritySpecs(): Promise<NodeAuthorityInfo[]> {
|
|
26
|
+
let nodes = await getAllNodeIds();
|
|
27
|
+
return Promise.all(nodes.map(async (nodeId): Promise<NodeAuthorityInfo> => {
|
|
28
|
+
let metadata = await timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, NodeCapabilitiesController.nodes[nodeId].getMetadata());
|
|
29
|
+
return { nodeId, entryPoint: metadata?.entryPoint, spec: metadata?.authoritySpec };
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const AuthoritySpecPageController = SocketFunction.register(
|
|
35
|
+
"AuthoritySpecPageController-7b8c9d0e-1f2a-3b4c-5d6e-7f8090a1b2c3",
|
|
36
|
+
new AuthoritySpecPageControllerBase(),
|
|
37
|
+
() => ({
|
|
38
|
+
getAllNodeAuthoritySpecs: {},
|
|
39
|
+
}),
|
|
40
|
+
() => ({
|
|
41
|
+
hooks: [assertIsManagementUser],
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
noAutoExpose: true,
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const AuthoritySpecSynced = getSyncedController(AuthoritySpecPageController, {
|
|
49
|
+
reads: { getAllNodeAuthoritySpecs: ["nodeAuthoritySpecs"] },
|
|
50
|
+
writes: {},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
class AuthorityRangeBar extends qreact.Component<{ start: number; end: number }> {
|
|
54
|
+
render() {
|
|
55
|
+
let startPct = this.props.start * 100;
|
|
56
|
+
let widthPct = (this.props.end - this.props.start) * 100;
|
|
57
|
+
return <div className={css.relative.size(RANGE_BAR_WIDTH_PX, RANGE_BAR_HEIGHT_PX).flexShrink0.hsl(0, 0, 92).bord2(0, 0, 75)}>
|
|
58
|
+
<div className={css.absolute.top(0).left(`${startPct}%`).size(`${widthPct}%`, "100%").hsl(210, 65, 50)} />
|
|
59
|
+
</div>;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo }> {
|
|
64
|
+
state = {
|
|
65
|
+
expanded: false,
|
|
66
|
+
};
|
|
67
|
+
render() {
|
|
68
|
+
let info = this.props.info;
|
|
69
|
+
let spec = info.spec!;
|
|
70
|
+
let expanded = this.state.expanded;
|
|
71
|
+
return <div
|
|
72
|
+
className={css.button.vbox(6).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}
|
|
73
|
+
onClick={() => this.state.expanded = !expanded}
|
|
74
|
+
>
|
|
75
|
+
<div className={css.hbox(10).fillWidth}>
|
|
76
|
+
<span>{expanded ? "▼" : "▶"}</span>
|
|
77
|
+
<span className={css.boldStyle}>{info.nodeId}</span>
|
|
78
|
+
<span>{spec.routeStart.toFixed(4)} - {spec.routeEnd.toFixed(4)}</span>
|
|
79
|
+
<AuthorityRangeBar start={spec.routeStart} end={spec.routeEnd} />
|
|
80
|
+
<span className={css.colorhsl(0, 0, 50)}>width {(spec.routeEnd - spec.routeStart).toFixed(4)}</span>
|
|
81
|
+
{spec.excludeDefault && <span className={css.colorhsl(0, 70, 35)}>(excludes default)</span>}
|
|
82
|
+
<span className={css.colorhsl(0, 0, 40).ellipsis.flexFillWidth}>{info.entryPoint || "(no entry point)"}</span>
|
|
83
|
+
</div>
|
|
84
|
+
{expanded &&
|
|
85
|
+
<div className={css.vbox(2)}>
|
|
86
|
+
<div className={css.boldStyle}>Prefixes ({spec.prefixes.length}):</div>
|
|
87
|
+
{spec.prefixes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(none)</div>}
|
|
88
|
+
{spec.prefixes.map(p =>
|
|
89
|
+
<div key={p.originalPrefix} className={css.colorhsl(0, 0, 25)}>{p.originalPrefix}</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
}
|
|
93
|
+
</div>;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class AuthoritySpecPage extends qreact.Component {
|
|
98
|
+
render() {
|
|
99
|
+
let infos = AuthoritySpecSynced(getBrowserUrlNode()).getAllNodeAuthoritySpecs();
|
|
100
|
+
if (!infos) {
|
|
101
|
+
return <div className={css.pad2(16)}>Loading authority specs...</div>;
|
|
102
|
+
}
|
|
103
|
+
infos = infos.filter(x => x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0);
|
|
104
|
+
sort(infos, x => x.spec!.routeStart);
|
|
105
|
+
|
|
106
|
+
return <div className={css.vbox(12).pad2(16).fillWidth}>
|
|
107
|
+
<h2>Node Authority Specs ({infos.length})</h2>
|
|
108
|
+
<div className={css.vbox(8).fillWidth}>
|
|
109
|
+
{infos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -333,8 +333,6 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
333
333
|
if (response.valid && response.time && compareTime(response.time, ourValue.time) > 0) {
|
|
334
334
|
valuesToRequest.push({ path: response.path, time: response.time });
|
|
335
335
|
let authorities = PathRouter.getAllAuthorities(response.path);
|
|
336
|
-
debugbreak(2);
|
|
337
|
-
debugger;
|
|
338
336
|
trackSyncAge({
|
|
339
337
|
path: response.path,
|
|
340
338
|
ourTimeId: ourValue.time.time,
|
|
@@ -349,8 +347,6 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
349
347
|
// - Send it our value
|
|
350
348
|
else if (response.valid === undefined && (!response.time || compareTime(ourValue.time, response.time) > 0)) {
|
|
351
349
|
valuesToSend.push(ourValue);
|
|
352
|
-
debugbreak(2);
|
|
353
|
-
debugger;
|
|
354
350
|
trackSyncAge({
|
|
355
351
|
path: response.path,
|
|
356
352
|
ourTimeId: ourValue.time.time,
|
|
@@ -370,8 +366,6 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
370
366
|
let age = now - ourValue.time.time;
|
|
371
367
|
if (age >= MAX_CHANGE_AGE) {
|
|
372
368
|
pathsToForceSync.add(response.path);
|
|
373
|
-
debugbreak(2);
|
|
374
|
-
debugger;
|
|
375
369
|
trackSyncAge({
|
|
376
370
|
path: response.path,
|
|
377
371
|
ourTimeId: ourValue.time.time,
|
package/test.ts
CHANGED
|
@@ -43,7 +43,8 @@ async function main() {
|
|
|
43
43
|
|
|
44
44
|
let nodes = await getAllNodeIds();
|
|
45
45
|
let values = await Promise.all(nodes.map(async node => {
|
|
46
|
-
|
|
46
|
+
let metadata = await timeoutToUndefinedSilent(5000, NodeCapabilitiesController.nodes[node].getMetadata());
|
|
47
|
+
return metadata?.entryPoint;
|
|
47
48
|
}));
|
|
48
49
|
console.log({ values });
|
|
49
50
|
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
using System;
|
|
2
|
-
using System.Diagnostics;
|
|
3
|
-
using System.Text;
|
|
4
|
-
using System.Runtime.InteropServices;
|
|
5
|
-
|
|
6
|
-
public class PPIDService
|
|
7
|
-
{
|
|
8
|
-
[DllImport("kernel32.dll")]
|
|
9
|
-
static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
|
|
10
|
-
|
|
11
|
-
[DllImport("kernel32.dll")]
|
|
12
|
-
static extern bool CloseHandle(IntPtr hObject);
|
|
13
|
-
|
|
14
|
-
[DllImport("ntdll.dll")]
|
|
15
|
-
static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref PROCESS_BASIC_INFORMATION pbi, int processInformationLength, out int returnLength);
|
|
16
|
-
|
|
17
|
-
[StructLayout(LayoutKind.Sequential)]
|
|
18
|
-
internal struct PROCESS_BASIC_INFORMATION
|
|
19
|
-
{
|
|
20
|
-
public IntPtr Reserved1;
|
|
21
|
-
public IntPtr PebBaseAddress;
|
|
22
|
-
public IntPtr Reserved2_0;
|
|
23
|
-
public IntPtr Reserved2_1;
|
|
24
|
-
public IntPtr UniqueProcessId;
|
|
25
|
-
public IntPtr InheritedFromUniqueProcessId;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
public static int GetParentProcessId(int pid)
|
|
29
|
-
{
|
|
30
|
-
IntPtr hProcess = OpenProcess(0x1000, false, pid);
|
|
31
|
-
if (hProcess == IntPtr.Zero)
|
|
32
|
-
{
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try
|
|
37
|
-
{
|
|
38
|
-
PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
|
|
39
|
-
int returnLength;
|
|
40
|
-
int status = NtQueryInformationProcess(hProcess, 0, ref pbi, Marshal.SizeOf(pbi), out returnLength);
|
|
41
|
-
if (status == 0)
|
|
42
|
-
{
|
|
43
|
-
return pbi.InheritedFromUniqueProcessId.ToInt32();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
finally
|
|
47
|
-
{
|
|
48
|
-
CloseHandle(hProcess);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
|
|
3
|
-
import { lazy } from "socket-function/src/caching";
|
|
4
|
-
import { runInSerial } from "socket-function/src/batching";
|
|
5
|
-
import child_process from "child_process";
|
|
6
|
-
import os from "os";
|
|
7
|
-
import readline from "readline";
|
|
8
|
-
import net from "net";
|
|
9
|
-
|
|
10
|
-
const windowsGetPPID = lazy(async () => {
|
|
11
|
-
const powershellProcess = child_process.spawn("powershell.exe", [
|
|
12
|
-
"-NoProfile",
|
|
13
|
-
"-NonInteractive",
|
|
14
|
-
"-Command",
|
|
15
|
-
"-",
|
|
16
|
-
], { stdio: ["pipe", "pipe", "pipe"] });
|
|
17
|
-
powershellProcess.stderr.pipe(process.stderr);
|
|
18
|
-
// powershellProcess.stdout.pipe(process.stdout);
|
|
19
|
-
|
|
20
|
-
powershellProcess.on("error", (error) => {
|
|
21
|
-
console.error("Failed to spawn powershell process", error);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
powershellProcess.stdin.write(`$inject = Get-Content -Raw -Path ${JSON.stringify(__dirname + "/getParentProcessId.cs")};\n`);
|
|
25
|
-
powershellProcess.stdin.write(`Add-Type -TypeDefinition $inject\n`);
|
|
26
|
-
|
|
27
|
-
return runInSerial((pid: number): Promise<number> => {
|
|
28
|
-
return new Promise((resolve) => {
|
|
29
|
-
powershellProcess.stdin.write(`[PPIDService]::GetParentProcessId(${pid})\n`);
|
|
30
|
-
powershellProcess.stdout.once("data", (data) => {
|
|
31
|
-
resolve(parseInt(data.toString()) || 0);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
export async function getPPID(pid: number): Promise<number> {
|
|
38
|
-
const platform = os.platform();
|
|
39
|
-
if (platform === "win32") {
|
|
40
|
-
let getPPID = await windowsGetPPID();
|
|
41
|
-
return getPPID(pid);
|
|
42
|
-
} else if (platform === "darwin" || platform === "linux") {
|
|
43
|
-
try {
|
|
44
|
-
const statContent = await fs.promises.readFile(`/proc/${pid}/stat`, "utf8");
|
|
45
|
-
const ppid = parseInt(statContent.split(" ")[3], 10);
|
|
46
|
-
return isNaN(ppid) ? 0 : ppid;
|
|
47
|
-
} catch (error) {
|
|
48
|
-
return 0;
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
throw new Error("Unsupported operating system");
|
|
52
|
-
}
|
|
53
|
-
}
|