@lincoln504/pi-research 0.1.12 → 1.0.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/LICENSE +1 -1
- package/README.md +64 -115
- package/dist/cli.mjs +23211 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +102 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +213 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +113 -0
- package/docs/THIRD-PARTY-NOTICES.md +64 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1044 -0
- package/src/config.ts +1036 -147
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +264 -7
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -174
- package/src/infrastructure/searxng-lifecycle.ts +0 -577
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Service
|
|
3
|
+
*
|
|
4
|
+
* Service wrapper that exposes the session-scoped metrics singleton through
|
|
5
|
+
* the service registry. All methods delegate to the global `metrics` export.
|
|
6
|
+
* The service owns no state of its own; metrics belong to the process lifetime
|
|
7
|
+
* and are managed by the SessionMetrics class in utils/metrics.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { IService } from '../core/service-registry.ts';
|
|
11
|
+
import { ServiceLifecycle } from '../core/service-registry.ts';
|
|
12
|
+
import { logger } from '../logger.ts';
|
|
13
|
+
import { metrics } from '../utils/metrics.ts';
|
|
14
|
+
import type { Labels, IMetricHistogram, IMetricsSnapshot } from '../utils/metrics.ts';
|
|
15
|
+
import { ServiceNames } from '../core/interfaces/service-names.ts';
|
|
16
|
+
|
|
17
|
+
export class MetricsService implements IService {
|
|
18
|
+
readonly name = ServiceNames.METRICS;
|
|
19
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
20
|
+
|
|
21
|
+
async initialize(): Promise<void> {
|
|
22
|
+
if (this.lifecycle === ServiceLifecycle.INITIALIZED) return;
|
|
23
|
+
this.lifecycle = ServiceLifecycle.INITIALIZING;
|
|
24
|
+
logger.debug('[MetricsService] Initializing...');
|
|
25
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
26
|
+
logger.debug('[MetricsService] Initialized');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async dispose(): Promise<void> {
|
|
30
|
+
if (this.lifecycle === ServiceLifecycle.DISPOSED) return;
|
|
31
|
+
this.lifecycle = ServiceLifecycle.DISPOSING;
|
|
32
|
+
logger.debug('[MetricsService] Disposing...');
|
|
33
|
+
// Session metrics belong to the process lifetime, not this service.
|
|
34
|
+
// Nothing to clean up — the process is exiting and memory is freed.
|
|
35
|
+
this.lifecycle = ServiceLifecycle.DISPOSED;
|
|
36
|
+
logger.debug('[MetricsService] Disposed');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
increment(name: string, value: number = 1, labels?: Labels): void {
|
|
40
|
+
metrics.increment(name, value, labels);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setGauge(name: string, value: number, labels?: Labels): void {
|
|
44
|
+
metrics.setGauge(name, value, labels);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
observe(name: string, value: number, labels?: Labels): void {
|
|
48
|
+
metrics.observe(name, value, labels);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async measure<T>(name: string, action: () => Promise<T>, labels?: Labels): Promise<T> {
|
|
52
|
+
return metrics.measure(name, action, labels);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getSnapshot(): IMetricsSnapshot {
|
|
56
|
+
return metrics.getSessionSnapshot();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
clear(): void {
|
|
60
|
+
metrics.clearSession();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getCounters(): Record<string, number> {
|
|
64
|
+
return metrics.getSessionSnapshot().counters ?? {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getGauges(): Record<string, number> {
|
|
68
|
+
return metrics.getSessionSnapshot().gauges ?? {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getHistograms(): Record<string, IMetricHistogram> {
|
|
72
|
+
return metrics.getSessionSnapshot().histograms ?? {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getCounter(name: string, labels?: Labels): number {
|
|
76
|
+
const counters = this.getCounters();
|
|
77
|
+
if (!labels) return counters[name] ?? 0;
|
|
78
|
+
const keys = Object.keys(labels).sort();
|
|
79
|
+
const labelStr = keys.map(k => `${k}="${labels[k]}"`).join(',');
|
|
80
|
+
const key = labelStr ? `${name}{${labelStr}}` : name;
|
|
81
|
+
return counters[key] ?? 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getGauge(name: string, labels?: Labels): number {
|
|
85
|
+
const gauges = this.getGauges();
|
|
86
|
+
if (!labels) return gauges[name] ?? 0;
|
|
87
|
+
const keys = Object.keys(labels).sort();
|
|
88
|
+
const labelStr = keys.map(k => `${k}="${labels[k]}"`).join(',');
|
|
89
|
+
const key = labelStr ? `${name}{${labelStr}}` : name;
|
|
90
|
+
return gauges[key] ?? 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getHistogramStats(name: string, labels?: Labels): IMetricHistogram | null {
|
|
94
|
+
const histograms = this.getHistograms();
|
|
95
|
+
if (!labels) return histograms[name] ?? null;
|
|
96
|
+
const keys = Object.keys(labels).sort();
|
|
97
|
+
const labelStr = keys.map(k => `${k}="${labels[k]}"`).join(',');
|
|
98
|
+
const key = labelStr ? `${name}{${labelStr}}` : name;
|
|
99
|
+
return histograms[key] ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
exportPrometheus(): string {
|
|
103
|
+
const snapshot = metrics.getSessionSnapshot();
|
|
104
|
+
const lines: string[] = [];
|
|
105
|
+
|
|
106
|
+
for (const [key, value] of Object.entries(snapshot.counters ?? {})) {
|
|
107
|
+
lines.push(`${key} ${value}`);
|
|
108
|
+
}
|
|
109
|
+
for (const [key, value] of Object.entries(snapshot.gauges ?? {})) {
|
|
110
|
+
lines.push(`${key} ${value}`);
|
|
111
|
+
}
|
|
112
|
+
for (const [key, s] of Object.entries(snapshot.histograms ?? {})) {
|
|
113
|
+
lines.push(`${key}_count ${s.count}`);
|
|
114
|
+
lines.push(`${key}_sum ${(s.avg * s.count).toFixed(2)}`);
|
|
115
|
+
lines.push(`${key}_min ${s.min}`);
|
|
116
|
+
lines.push(`${key}_max ${s.max}`);
|
|
117
|
+
lines.push(`${key}_avg ${s.avg.toFixed(2)}`);
|
|
118
|
+
lines.push(`${key}_p50 ${s.p50.toFixed(2)}`);
|
|
119
|
+
lines.push(`${key}_p90 ${s.p90.toFixed(2)}`);
|
|
120
|
+
lines.push(`${key}_p95 ${s.p95.toFixed(2)}`);
|
|
121
|
+
lines.push(`${key}_p99 ${s.p99.toFixed(2)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Re-export for consumers that import from this module
|
|
129
|
+
export { type Labels, MetricsRegistry } from '../utils/metrics.ts';
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Lifecycle Service
|
|
3
|
+
*
|
|
4
|
+
* Provides PID checks and process monitoring functionality.
|
|
5
|
+
* Stateless service for checking if processes are alive.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs/promises';
|
|
9
|
+
import { execFile } from 'node:child_process';
|
|
10
|
+
import type { IProcessLifecycle } from '../core/interfaces/process-interfaces.ts';
|
|
11
|
+
import { ServiceLifecycle } from '../core/service-registry.ts';
|
|
12
|
+
import { ServiceNames } from '../core/interfaces/service-names.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Process Lifecycle Service
|
|
16
|
+
*
|
|
17
|
+
* Provides utilities for checking process liveness and monitoring.
|
|
18
|
+
*/
|
|
19
|
+
export class ProcessLifecycleService implements IProcessLifecycle {
|
|
20
|
+
readonly name = ServiceNames.PROCESS_LIFECYCLE;
|
|
21
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
22
|
+
|
|
23
|
+
private cachedStartTime: number | null = null;
|
|
24
|
+
|
|
25
|
+
async initialize(): Promise<void> {
|
|
26
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async dispose(): Promise<void> {
|
|
30
|
+
// Stateless service
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the start time of a process (Linux only).
|
|
35
|
+
* Combined with PID, this provides a globally unique identifier for a process
|
|
36
|
+
* even if PIDs are recycled by the OS.
|
|
37
|
+
*
|
|
38
|
+
* @param pid Process ID
|
|
39
|
+
* @returns Start time identifier (Linux: jiffies; cross-platform: epoch seconds) or null
|
|
40
|
+
*/
|
|
41
|
+
async getProcessStartTime(pid: number): Promise<number | null> {
|
|
42
|
+
// Linux: read from /proc/{pid}/stat (field 22 = starttime in jiffies)
|
|
43
|
+
if (process.platform === 'linux') {
|
|
44
|
+
try {
|
|
45
|
+
const stat = await fs.readFile(`/proc/${pid}/stat`, 'utf8');
|
|
46
|
+
const lastParenIndex = stat.lastIndexOf(')');
|
|
47
|
+
if (lastParenIndex === -1) return null;
|
|
48
|
+
const partsAfterName = stat.substring(lastParenIndex + 2).trim().split(/\s+/);
|
|
49
|
+
const startTimeStr = partsAfterName[19];
|
|
50
|
+
return startTimeStr ? parseInt(startTimeStr, 10) : null;
|
|
51
|
+
} catch (_err) {
|
|
52
|
+
// ENOENT is common if process just died
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Windows: use powershell to get process creation time
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
try {
|
|
60
|
+
const output = await new Promise<string>((resolve, reject) => {
|
|
61
|
+
const child = execFile(
|
|
62
|
+
'powershell',
|
|
63
|
+
['-NoProfile', '-Command', `[Math]::Floor(([DateTimeOffset](Get-Process -Id ${pid} -ErrorAction Stop).StartTime).ToUnixTimeSeconds())`],
|
|
64
|
+
{ encoding: 'utf8', timeout: 5000 },
|
|
65
|
+
(err, stdout) => err ? reject(err) : resolve(stdout),
|
|
66
|
+
);
|
|
67
|
+
child.unref?.();
|
|
68
|
+
});
|
|
69
|
+
if (!output || !output.trim()) return null;
|
|
70
|
+
const startTime = parseInt(output.trim(), 10);
|
|
71
|
+
return isNaN(startTime) ? null : startTime;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Cross-platform fallback (macOS/Unix): `ps -o etimes=` returns elapsed seconds.
|
|
78
|
+
try {
|
|
79
|
+
const output = await new Promise<string>((resolve, reject) => {
|
|
80
|
+
const child = execFile(
|
|
81
|
+
'ps', ['-o', 'etimes=', '-p', String(pid)],
|
|
82
|
+
{ encoding: 'utf8', timeout: 3000 },
|
|
83
|
+
(err, stdout) => err ? reject(err) : resolve(stdout),
|
|
84
|
+
);
|
|
85
|
+
child.unref?.();
|
|
86
|
+
});
|
|
87
|
+
if (!output || !output.trim()) return null;
|
|
88
|
+
const elapsedSec = parseInt(output.trim(), 10);
|
|
89
|
+
if (isNaN(elapsedSec)) return null;
|
|
90
|
+
return Math.floor(Date.now() / 1000) - elapsedSec;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the start time for the current process
|
|
98
|
+
*/
|
|
99
|
+
async getCurrentProcessStartTime(): Promise<number | null> {
|
|
100
|
+
if (this.cachedStartTime !== null) return this.cachedStartTime;
|
|
101
|
+
this.cachedStartTime = await this.getProcessStartTime(process.pid);
|
|
102
|
+
return this.cachedStartTime;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a process is alive by sending signal 0.
|
|
107
|
+
* Optionally verifies that the start time matches to prevent PID reuse races.
|
|
108
|
+
*
|
|
109
|
+
* @param pid Process ID to check
|
|
110
|
+
* @param expectedStartTime Optional start time to verify (from getProcessStartTime)
|
|
111
|
+
* @returns true if process is alive (and matches start time if provided)
|
|
112
|
+
*/
|
|
113
|
+
async isProcessAlive(pid: number, expectedStartTime?: number | null): Promise<boolean> {
|
|
114
|
+
try {
|
|
115
|
+
// 1. Basic liveness check via signal 0
|
|
116
|
+
process.kill(pid, 0);
|
|
117
|
+
|
|
118
|
+
// 2. If start time verification requested, check it
|
|
119
|
+
if (expectedStartTime !== undefined && expectedStartTime !== null) {
|
|
120
|
+
const actualStartTime = await this.getProcessStartTime(pid);
|
|
121
|
+
if (actualStartTime === null) {
|
|
122
|
+
// If we can't get start time but expected it, we can't be sure it's the same process
|
|
123
|
+
// On Linux this usually means the process just exited between the signal and the read.
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
// Allow ±1s slack. The macOS/BSD fallback derives start time as
|
|
127
|
+
// floor(Date.now()/1000) - `ps etimes` (elapsed whole seconds); both terms are
|
|
128
|
+
// independently quantized, so the SAME process can compute start times 1s apart
|
|
129
|
+
// across two reads. A strict === there makes a live lock/leader owner read as
|
|
130
|
+
// dead → the lock is reclaimed from under a live writer (lost update). PID reuse
|
|
131
|
+
// within a 1s window at the identical PID is not a realistic false-match.
|
|
132
|
+
return Math.abs(actualStartTime - expectedStartTime) <= 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sync version of basic liveness check (no start-time verification)
|
|
143
|
+
*/
|
|
144
|
+
isProcessAliveSync(pid: number): boolean {
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 0);
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if a process is alive with optional scheduler ID verification.
|
|
155
|
+
* This method is designed to work with state that contains scheduler information.
|
|
156
|
+
*
|
|
157
|
+
* @param pid Process ID to check
|
|
158
|
+
* @param expectedSchedulerId Optional scheduler ID to verify
|
|
159
|
+
* @param getState Function to retrieve current state for scheduler ID verification
|
|
160
|
+
* @param skipLock If true, skip locking when retrieving state (useful to prevent deadlocks)
|
|
161
|
+
* @returns true if process is alive and (if provided) matches expected scheduler ID
|
|
162
|
+
*/
|
|
163
|
+
async isPidAlive<TState>(
|
|
164
|
+
pid: number,
|
|
165
|
+
expectedSchedulerId?: string,
|
|
166
|
+
options?: {
|
|
167
|
+
getState?: (skipLock?: boolean) => Promise<TState>;
|
|
168
|
+
skipLock?: boolean;
|
|
169
|
+
getSchedulerIdFromState?: (state: TState) => string | undefined;
|
|
170
|
+
expectedStartTime?: number | null;
|
|
171
|
+
}
|
|
172
|
+
): Promise<boolean> {
|
|
173
|
+
const alive = await this.isProcessAlive(pid, options?.expectedStartTime);
|
|
174
|
+
if (!alive) return false;
|
|
175
|
+
|
|
176
|
+
if (expectedSchedulerId && options?.getState && options?.getSchedulerIdFromState) {
|
|
177
|
+
// Use skipLock if true to prevent deadlocks when called inside updateState
|
|
178
|
+
const state = await options.getState(options.skipLock);
|
|
179
|
+
const schedulerId = options.getSchedulerIdFromState(state);
|
|
180
|
+
return schedulerId === expectedSchedulerId;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get the current process ID
|
|
188
|
+
*/
|
|
189
|
+
getCurrentPid(): number {
|
|
190
|
+
return process.pid;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Wait for a process to terminate
|
|
195
|
+
*
|
|
196
|
+
* @param pid Process ID to wait for
|
|
197
|
+
* @param timeoutMs Maximum time to wait in milliseconds
|
|
198
|
+
* @param checkIntervalMs Interval between checks in milliseconds
|
|
199
|
+
* @param expectedStartTime Optional start time to verify
|
|
200
|
+
* @returns true if process terminated, false if timeout
|
|
201
|
+
*/
|
|
202
|
+
async waitForProcessTermination(
|
|
203
|
+
pid: number,
|
|
204
|
+
timeoutMs: number = 5000,
|
|
205
|
+
checkIntervalMs: number = 100,
|
|
206
|
+
expectedStartTime?: number | null
|
|
207
|
+
): Promise<boolean> {
|
|
208
|
+
const startTime = Date.now();
|
|
209
|
+
|
|
210
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
211
|
+
if (!(await this.isProcessAlive(pid, expectedStartTime))) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
await this.sleep(checkIntervalMs);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if the current process is the one with the given PID
|
|
222
|
+
*
|
|
223
|
+
* @param pid Process ID to check
|
|
224
|
+
* @returns true if the current process has the given PID
|
|
225
|
+
*/
|
|
226
|
+
isCurrentProcess(pid: number): boolean {
|
|
227
|
+
return process.pid === pid;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get process information (platform-specific)
|
|
232
|
+
*
|
|
233
|
+
* @param pid Process ID to get information for
|
|
234
|
+
* @returns Process information or null if not available
|
|
235
|
+
*/
|
|
236
|
+
async getProcessInfo(pid: number): Promise<{ pid: number; alive: boolean; startTime?: number | null } | null> {
|
|
237
|
+
const startTime = await this.getProcessStartTime(pid);
|
|
238
|
+
const alive = await this.isProcessAlive(pid, startTime);
|
|
239
|
+
return {
|
|
240
|
+
pid,
|
|
241
|
+
alive,
|
|
242
|
+
startTime,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Sleep for a specified number of milliseconds
|
|
248
|
+
* @param ms The number of milliseconds to sleep
|
|
249
|
+
*/
|
|
250
|
+
private sleep(ms: number): Promise<void> {
|
|
251
|
+
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler Factory Service
|
|
3
|
+
*
|
|
4
|
+
* Implements the ISchedulerFactory interface for creating scheduler instances.
|
|
5
|
+
* This module lives in the Infrastructure layer and provides concrete implementations
|
|
6
|
+
* of scheduler creation logic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Config } from '../config.ts';
|
|
10
|
+
import { ServiceLifecycle, getServiceContainer, type ServiceContainer } from '../core/service-registry.ts';
|
|
11
|
+
import type { IService } from '../core/service-registry.ts';
|
|
12
|
+
import type { ISchedulerFactory, IScheduler } from '../core/scheduler-factory.ts';
|
|
13
|
+
import { getScheduler as _getScheduler, forceSchedulerRestart as _forceSchedulerRestart, disposeBrowserInitLock } from './browser/scheduler-factory.ts';
|
|
14
|
+
import { getSchedulerVersion as _getSchedulerVersion } from './browser/config.ts';
|
|
15
|
+
import { ServiceNames } from '../core/interfaces/service-names.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Scheduler Factory Service Implementation
|
|
19
|
+
*/
|
|
20
|
+
export class SchedulerFactoryService implements ISchedulerFactory, IService {
|
|
21
|
+
readonly name = ServiceNames.SCHEDULER_FACTORY;
|
|
22
|
+
lifecycle = ServiceLifecycle.UNINITIALIZED;
|
|
23
|
+
private container: ServiceContainer = getServiceContainer();
|
|
24
|
+
|
|
25
|
+
async initialize(ctx?: any): Promise<void> {
|
|
26
|
+
if (ctx?.container) {
|
|
27
|
+
this.container = ctx.container;
|
|
28
|
+
}
|
|
29
|
+
this.lifecycle = ServiceLifecycle.INITIALIZED;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async dispose(): Promise<void> {
|
|
33
|
+
await disposeBrowserInitLock(this.container);
|
|
34
|
+
this.lifecycle = ServiceLifecycle.DISPOSED;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get or create a scheduler instance.
|
|
39
|
+
* Handles leader election and client/server mode switching.
|
|
40
|
+
*/
|
|
41
|
+
async getScheduler(config?: Config): Promise<IScheduler> {
|
|
42
|
+
return _getScheduler(config, this.container);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the current scheduler version string.
|
|
47
|
+
*/
|
|
48
|
+
getSchedulerVersion(config?: Config): string {
|
|
49
|
+
return _getSchedulerVersion(config);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Force a restart of the scheduler by clearing the global cache and state.
|
|
54
|
+
*/
|
|
55
|
+
async forceSchedulerRestart(forceClearRemoteState?: boolean): Promise<void> {
|
|
56
|
+
return _forceSchedulerRestart(forceClearRemoteState, this.container);
|
|
57
|
+
}
|
|
58
|
+
}
|