querysub 0.450.0 → 0.452.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.
@@ -79,10 +79,11 @@ export class NodeViewer extends qreact.Component {
79
79
  let data: NodeData = { nodeId };
80
80
  try {
81
81
  data.table = await controller.getMiscInfo(nodeId);
82
- data.live_isAlive = await controller.live_isAlive(nodeId);
83
- data.live_exposedControllers = await controller.getExposedControllers(nodeId);
84
- data.live_entryPoint = await controller.live_getEntryPoint(nodeId);
85
- data.live_trueTimeOffset = await controller.live_getTrueTimeOffset(nodeId);
82
+ let metadata = await controller.live_getMetadata(nodeId);
83
+ data.live_isAlive = !!metadata;
84
+ data.live_exposedControllers = metadata?.exposedControllers;
85
+ data.live_entryPoint = metadata?.entryPoint;
86
+ data.live_trueTimeOffset = metadata?.trueTimeOffset;
86
87
  data.live_authorityPaths = await controller.live_getAuthorityPaths(nodeId);
87
88
  data.ip = await controller.getNodeIP(nodeId);
88
89
  if (data.ip === ourExternalIP || data.ip === ourIP) {
@@ -484,27 +485,18 @@ class NodeViewerControllerBase {
484
485
  await syncNodesNow();
485
486
  }
486
487
 
487
- public async getExposedControllers(nodeId: string) {
488
- return await NodeCapabilitiesController.nodes[nodeId].getExposedControllers();
488
+ public async live_getMetadata(nodeId: string) {
489
+ return await errorToUndefinedSilent(NodeCapabilitiesController.nodes[nodeId].getMetadata());
489
490
  }
490
491
 
491
492
  public async getControllerNodeIdList(controller: SocketRegistered<{}>) {
492
493
  return await getControllerNodeIdList(controller);
493
494
  }
494
495
 
495
- public async live_isAlive(nodeId: string) {
496
- return !!await errorToUndefinedSilent(NodeCapabilitiesController.nodes[nodeId].getEntryPoint());
497
- }
498
496
  public async live_getAuthorityPaths(nodeId: string) {
499
497
  let topo = await NodeMetadataController.nodes[nodeId].debugGetTopologyEntry(nodeId);
500
498
  return topo?.authoritySpec;
501
499
  }
502
- public async live_getEntryPoint(nodeId: string) {
503
- return NodeCapabilitiesController.nodes[nodeId].getEntryPoint();
504
- }
505
- public async live_getTrueTimeOffset(nodeId: string) {
506
- return NodeCapabilitiesController.nodes[nodeId].getTrueTimeOffset();
507
- }
508
500
 
509
501
  public async getMiscInfo(nodeId: string): Promise<{
510
502
  columns: ColumnsType;
@@ -521,18 +513,19 @@ class NodeViewerControllerBase {
521
513
  row[columnName] = "Error: " + e.stack;
522
514
  }
523
515
  }
516
+ let metadataPromise = NodeCapabilitiesController.nodes[nodeId].getMetadata();
524
517
  let promises = [
525
518
  wrapAddTableValue("paths|paths", {}, async () => {
526
519
  let live_authorityPaths = (await NodeMetadataController.nodes[nodeId].debugGetPathAuthorities(nodeId));
527
520
  return JSON.stringify(live_authorityPaths);
528
521
  }),
529
522
  wrapAddTableValue("paths|functions", {}, async () => {
530
- let live_functionRunnerShards = await NodeCapabilitiesController.nodes[nodeId].getFunctionRunnerShards();
531
- return live_functionRunnerShards.map(x => `${x.domainName}[${x.shardRange.startFraction}-${x.shardRange.endFraction}]${x.secondaryShardRange && `+[${x.secondaryShardRange?.startFraction || 0}-${x.secondaryShardRange?.endFraction || 0}]` || ""}`).join(" | ");
523
+ let metadata = await metadataPromise;
524
+ return metadata.functionRunnerShards.map(x => `${x.domainName}[${x.shardRange.startFraction}-${x.shardRange.endFraction}]${x.secondaryShardRange && `+[${x.secondaryShardRange?.startFraction || 0}-${x.secondaryShardRange?.endFraction || 0}]` || ""}`).join(" | ");
532
525
  }),
533
526
  wrapAddTableValue("uptime", { formatter: "timeSpan" }, async () => {
534
- let startupTime = await NodeCapabilitiesController.nodes[nodeId].getStartupTime();
535
- return Date.now() - startupTime;
527
+ let metadata = await metadataPromise;
528
+ return Date.now() - metadata.startupTime;
536
529
  }),
537
530
  wrapAddTableValue("port", {}, async () => {
538
531
  return nodeId.split(":").at(-1);
@@ -557,8 +550,8 @@ class NodeViewerControllerBase {
557
550
  ].filter(x => x);
558
551
  }),
559
552
  wrapAddTableValue("capabilities|capabilities", {}, async () => {
560
- let live_exposedControllers = await NodeCapabilitiesController.nodes[nodeId].getExposedControllers();
561
- return live_exposedControllers.map(x => x.split("-")[0]);
553
+ let metadata = await metadataPromise;
554
+ return metadata.exposedControllers.map(x => x.split("-")[0]);
562
555
  }),
563
556
  ];
564
557
  await Promise.allSettled(promises);
@@ -590,12 +583,9 @@ export const NodeViewerController = SocketFunction.register(
590
583
  getExternalInspectURL: {},
591
584
  verifyAccess: {},
592
585
  getAllNodeIds: {},
593
- getExposedControllers: {},
594
586
  getControllerNodeIdList: {},
595
- live_isAlive: {},
587
+ live_getMetadata: {},
596
588
  live_getAuthorityPaths: {},
597
- live_getEntryPoint: {},
598
- live_getTrueTimeOffset: {},
599
589
  getMiscInfo: {},
600
590
 
601
591
  forceRefreshNodes: {},
@@ -168,17 +168,17 @@ async function getNodeInfos(): Promise<NodeInfo[]> {
168
168
  const nodes = await getAllNodeIds();
169
169
  return Promise.all(
170
170
  nodes.map(async (nodeId): Promise<NodeInfo> => {
171
- const [entryPoint, inspectUrl] = await Promise.all([
171
+ const [metadata, inspectUrl] = await Promise.all([
172
172
  timeoutToUndefinedSilent(
173
173
  NODE_INFO_TIMEOUT_MS,
174
- NodeCapabilitiesController.nodes[nodeId].getEntryPoint(),
174
+ NodeCapabilitiesController.nodes[nodeId].getMetadata(),
175
175
  ),
176
176
  timeoutToUndefinedSilent(
177
177
  NODE_INFO_TIMEOUT_MS,
178
178
  NodeCapabilitiesController.nodes[nodeId].getInspectURL(),
179
179
  ),
180
180
  ]);
181
- return { nodeId, entryPoint, inspectUrl };
181
+ return { nodeId, entryPoint: metadata?.entryPoint, inspectUrl };
182
182
  }),
183
183
  );
184
184
  }
@@ -290,8 +290,8 @@ export class IndexedLogs<T> {
290
290
  if (!hasLogger) return false;
291
291
  // NOTE: Prefer to do the searching on the move logs service. However, if it's not available, any service can do searching. It just might lag that server...
292
292
  if (preferredOnly) {
293
- let entryPoint = await timeoutToUndefinedSilent(2500, NodeCapabilitiesController.nodes[nodeId].getEntryPoint());
294
- if (!entryPoint?.includes("movelogs")) return false;
293
+ let metadata = await timeoutToUndefinedSilent(2500, NodeCapabilitiesController.nodes[nodeId].getMetadata());
294
+ if (!metadata?.entryPoint.includes("movelogs")) return false;
295
295
  }
296
296
  added = true;
297
297
 
@@ -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 moved-to-public.
146
- private movedThroughByMachine = new Map<string, number>();
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
- 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)"}`);
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(LOGGER_NAMES.map(async (loggerName) => {
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 LOGGER_NAMES) {
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 whether it
454
- // has pending logs overlapping [0, endTime]. The first node that answers
455
- // without throwing wins; if it says yes, we ask the same node to flush.
456
- // We iterate because not every node necessarily exposes the new endpoints
457
- // (e.g. older versions still running). Records moved-through up to
458
- // now - MOVE_GRACE so we skip this on subsequent calls covering the same
459
- // window.
460
- private async ensureMovedThrough(machineId: string, endTime: number): Promise<"cached" | "no-node" | "moved"> {
461
- let lastMoved = this.movedThroughByMachine.get(machineId) ?? 0;
462
- if (lastMoved >= endTime) return "cached";
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 LOGGER_NAMES) {
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
- this.movedThroughByMachine.set(machineId, Date.now() - MOVE_GRACE);
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 entry = await timeoutToUndefinedSilent(
556
+ let metadata = await timeoutToUndefinedSilent(
515
557
  2500,
516
- NodeCapabilitiesController.nodes[nodeId].getEntryPoint(),
558
+ NodeCapabilitiesController.nodes[nodeId].getMetadata(),
517
559
  );
518
- if (entry?.includes("movelogs")) preferred.push(nodeId);
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
 
@@ -40,6 +40,7 @@ let pages: {
40
40
  componentName: string;
41
41
  title: string;
42
42
  }[] = [];
43
+ let headerNotifications: preact.ComponentClass[] = [];
43
44
 
44
45
  let __schema: SchemaObject<unknown, {
45
46
  isManagementUser: () => boolean;
@@ -63,6 +64,9 @@ export async function registerManagementPages2(config: {
63
64
  controllerName?: string;
64
65
  getModule: () => Promise<any>;
65
66
 
67
+ // If provided, this component (exported from getModule) is rendered in the management header.
68
+ notificationComponentName?: string;
69
+
66
70
  title?: string;
67
71
  }[];
68
72
  }) {
@@ -89,6 +93,12 @@ export async function registerManagementPages2(config: {
89
93
  controllerName: "NodeViewerController",
90
94
  getModule: () => import("./NodeViewer"),
91
95
  });
96
+ inputPages.push({
97
+ title: "Authority Specs",
98
+ componentName: "AuthoritySpecPage",
99
+ controllerName: "AuthoritySpecPageController",
100
+ getModule: () => import("./misc-pages/AuthoritySpecPage"),
101
+ });
92
102
  inputPages.push({
93
103
  title: "LOG VIEWER",
94
104
  componentName: "LogViewer3",
@@ -240,6 +250,9 @@ export async function registerManagementPages2(config: {
240
250
  componentName: page.componentName,
241
251
  componentClass: createLazyComponent(page.getModule)(page.componentName),
242
252
  });
253
+ if (page.notificationComponentName) {
254
+ headerNotifications.push(createLazyComponent(page.getModule)(page.notificationComponentName));
255
+ }
243
256
  }
244
257
  }
245
258
  }
@@ -299,6 +312,7 @@ export function renderIsManagementUser() {
299
312
  }
300
313
 
301
314
  const ErrorWarning = createLazyComponent(() => import("./logs/errorNotifications2/ErrorWarning"))("ErrorWarning");
315
+ const LaunchTrackingHeader = createLazyComponent(() => import("../deployManager/LaunchTrackingHeader"))("LaunchTrackingHeader");
302
316
 
303
317
  class ManagementRoot extends qreact.Component {
304
318
  state = {
@@ -355,6 +369,7 @@ class ManagementRoot extends qreact.Component {
355
369
  </style>
356
370
  <div class={css.fillWidth.hbox(20, 4).wrap.hsl(245, 25, 80).pad2(20, 4).pointerEvents("all")}>
357
371
  <ErrorWarning />
372
+ {headerNotifications.map(N => <N />)}
358
373
  <ATag values={[
359
374
  managementPageURL.getOverride("MachinesPage"),
360
375
  currentViewParam.getOverride("services"),
@@ -370,6 +385,7 @@ class ManagementRoot extends qreact.Component {
370
385
  {isCurrentUserSuperUser() && <div className={css.vbox(4)}>
371
386
  <PathDistributionInfo />
372
387
  <ValuePathWarning />
388
+ <LaunchTrackingHeader />
373
389
  </div>}
374
390
  </div>
375
391
  {currentPage &&
@@ -0,0 +1,112 @@
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
+ render() {
65
+ let info = this.props.info;
66
+ let spec = info.spec;
67
+ return <div className={css.vbox(6).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}>
68
+ <div className={css.hbox(10).fillWidth}>
69
+ <span className={css.boldStyle}>{info.nodeId}</span>
70
+ <span className={css.colorhsl(0, 0, 40).ellipsis.flexFillWidth}>{info.entryPoint || "(no entry point)"}</span>
71
+ </div>
72
+ {!spec &&
73
+ <div className={css.colorhsl(0, 60, 35)}>(no authority spec returned)</div>
74
+ }
75
+ {spec &&
76
+ <div className={css.vbox(6).fillWidth}>
77
+ <div className={css.hbox(10)}>
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
+ </div>
83
+ <div className={css.vbox(2)}>
84
+ <div className={css.boldStyle}>Prefixes ({spec.prefixes.length}):</div>
85
+ {spec.prefixes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(none)</div>}
86
+ {spec.prefixes.map(p =>
87
+ <div key={p.originalPrefix} className={css.colorhsl(0, 0, 25)}>{p.originalPrefix}</div>
88
+ )}
89
+ </div>
90
+ </div>
91
+ }
92
+ </div>;
93
+ }
94
+ }
95
+
96
+ export class AuthoritySpecPage extends qreact.Component {
97
+ render() {
98
+ let infos = AuthoritySpecSynced(getBrowserUrlNode()).getAllNodeAuthoritySpecs();
99
+ if (!infos) {
100
+ return <div className={css.pad2(16)}>Loading authority specs...</div>;
101
+ }
102
+ let sorted = [...infos];
103
+ sort(sorted, x => x.spec ? x.spec.routeStart : 2);
104
+
105
+ return <div className={css.vbox(12).pad2(16).fillWidth}>
106
+ <h2>Node Authority Specs ({infos.length})</h2>
107
+ <div className={css.vbox(8).fillWidth}>
108
+ {sorted.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
109
+ </div>
110
+ </div>;
111
+ }
112
+ }
@@ -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
- return timeoutToUndefinedSilent(5000, NodeCapabilitiesController.nodes[node].getEntryPoint());
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
- }