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.
@@ -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
 
@@ -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
- 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
- }
@@ -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
- }