@lincoln504/pi-research 0.1.13 → 1.0.1
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 +55 -138
- package/dist/cli.mjs +23375 -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 +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -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 +1195 -0
- package/src/config.ts +1032 -155
- 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 +258 -11
- 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 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- 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
|
@@ -1,1183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SearXNG Container Manager - Simplified Design
|
|
3
|
-
*
|
|
4
|
-
* Singleton-style SearXNG container management shared by research sessions.
|
|
5
|
-
*
|
|
6
|
-
* DESIGN PRINCIPLES:
|
|
7
|
-
* 1. Shared container reuse across research sessions
|
|
8
|
-
* 2. Lazy startup on first use
|
|
9
|
-
* 3. Explicit extension-owned cleanup when the extension shuts down
|
|
10
|
-
* 4. Simple error recovery - if container is gone, recreate it
|
|
11
|
-
* 5. No AutoRemove - manage cleanup explicitly
|
|
12
|
-
* 6. No complex reuse logic - just check for container by name
|
|
13
|
-
* 7. Manage Docker images - pull latest, prune old images on start
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import Docker from 'dockerode';
|
|
17
|
-
import * as fs from 'node:fs';
|
|
18
|
-
import * as net from 'node:net';
|
|
19
|
-
import * as path from 'node:path';
|
|
20
|
-
import { StateManager } from './state-manager.ts';
|
|
21
|
-
import { logger } from '../logger.ts';
|
|
22
|
-
|
|
23
|
-
// Docker type definitions
|
|
24
|
-
interface DockerContainerInspectInfo {
|
|
25
|
-
State: {
|
|
26
|
-
Running: boolean;
|
|
27
|
-
Status: string;
|
|
28
|
-
};
|
|
29
|
-
NetworkSettings: {
|
|
30
|
-
Ports: Record<string, Array<{ HostPort: string }> | null> | null;
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface DockerContainerListInfo {
|
|
35
|
-
Id: string;
|
|
36
|
-
Names: string[];
|
|
37
|
-
Image: string;
|
|
38
|
-
State: string;
|
|
39
|
-
Status: string;
|
|
40
|
-
RepoTags?: string[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface DockerImageInfo {
|
|
44
|
-
Id: string;
|
|
45
|
-
RepoTags?: string[];
|
|
46
|
-
Created: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface DockerContainerCreateOptions {
|
|
50
|
-
name: string;
|
|
51
|
-
Image: string;
|
|
52
|
-
ExposedPorts: Record<string, {}>;
|
|
53
|
-
HostConfig: {
|
|
54
|
-
PortBindings: Record<string, Array<{ HostPort: string }>>;
|
|
55
|
-
Binds: string[];
|
|
56
|
-
AutoRemove: boolean;
|
|
57
|
-
ExtraHosts?: string[];
|
|
58
|
-
};
|
|
59
|
-
Env: string[];
|
|
60
|
-
Labels: Record<string, string>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export const DOCKER_HOST_INTERNAL_HOSTNAME = 'host.docker.internal';
|
|
64
|
-
|
|
65
|
-
// Extension context type
|
|
66
|
-
interface ExtensionContext {
|
|
67
|
-
ui?: {
|
|
68
|
-
notify(_message: string, _type?: 'info' | 'warning' | 'error'): void;
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Default configuration
|
|
73
|
-
const DEFAULT_CONFIG = {
|
|
74
|
-
port: 55732,
|
|
75
|
-
imageName: 'searxng/searxng',
|
|
76
|
-
imageTag: 'latest',
|
|
77
|
-
healthTimeout: 120000,
|
|
78
|
-
pruneOldImages: true,
|
|
79
|
-
containerName: 'pi-searxng',
|
|
80
|
-
} as const;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Configuration from environment variables
|
|
84
|
-
*/
|
|
85
|
-
interface EnvConfig {
|
|
86
|
-
port?: number;
|
|
87
|
-
imageName?: string;
|
|
88
|
-
imageTag?: string;
|
|
89
|
-
healthTimeout?: number;
|
|
90
|
-
containerName?: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
type DockerConnectionOptions = ConstructorParameters<typeof Docker>[0];
|
|
94
|
-
|
|
95
|
-
function toDockerBindPath(p: string): string {
|
|
96
|
-
return process.platform === 'win32' ? p.replace(/\\/g, '/') : p;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function normalizeSocketPath(socketPath: string): string {
|
|
100
|
-
const trimmed = socketPath.trim();
|
|
101
|
-
if (trimmed.startsWith('\\\\.\\pipe\\')) {
|
|
102
|
-
return `//./pipe/${trimmed.slice('\\\\.\\pipe\\'.length)}`;
|
|
103
|
-
}
|
|
104
|
-
return trimmed;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function parseDockerHost(host: string): DockerConnectionOptions {
|
|
108
|
-
if (host.startsWith('unix://')) {
|
|
109
|
-
return { socketPath: host.slice('unix://'.length) };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (host.startsWith('npipe://')) {
|
|
113
|
-
return { socketPath: normalizeSocketPath(host.slice('npipe://'.length)) };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const url = new URL(host);
|
|
117
|
-
const protocol = url.protocol.replace(':', '');
|
|
118
|
-
return {
|
|
119
|
-
host: url.hostname,
|
|
120
|
-
port: url.port ? parseInt(url.port, 10) : undefined,
|
|
121
|
-
protocol: protocol === 'https' ? 'https' : protocol === 'ssh' ? 'ssh' : 'http',
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function getDockerConnectionCandidates(
|
|
126
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
127
|
-
): DockerConnectionOptions[] {
|
|
128
|
-
const candidates: DockerConnectionOptions[] = [];
|
|
129
|
-
const socketOverride = env['DOCKER_SOCKET']?.trim();
|
|
130
|
-
if (socketOverride) {
|
|
131
|
-
candidates.push({ socketPath: normalizeSocketPath(socketOverride) });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const hostOverride = env['DOCKER_HOST']?.trim();
|
|
135
|
-
if (hostOverride) {
|
|
136
|
-
try {
|
|
137
|
-
candidates.push(parseDockerHost(hostOverride));
|
|
138
|
-
} catch {
|
|
139
|
-
// Ignore malformed DOCKER_HOST and fall through to platform defaults.
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (process.platform === 'win32') {
|
|
144
|
-
candidates.push(
|
|
145
|
-
{ socketPath: '//./pipe/docker_engine' },
|
|
146
|
-
{ socketPath: '//./pipe/dockerDesktopLinuxEngine' },
|
|
147
|
-
{ socketPath: '//./pipe/dockerDesktopWindowsEngine' },
|
|
148
|
-
);
|
|
149
|
-
} else {
|
|
150
|
-
candidates.push({ socketPath: '/var/run/docker.sock' });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const seen = new Set<string>();
|
|
154
|
-
return candidates.filter((candidate) => {
|
|
155
|
-
const key = JSON.stringify(candidate);
|
|
156
|
-
if (seen.has(key)) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
seen.add(key);
|
|
160
|
-
return true;
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function getDockerHostGatewayExtraHosts(platform: NodeJS.Platform = process.platform): string[] {
|
|
165
|
-
return platform === 'linux' ? [`${DOCKER_HOST_INTERNAL_HOSTNAME}:host-gateway`] : [];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Load configuration from environment variables
|
|
170
|
-
*/
|
|
171
|
-
function loadConfigFromEnv(): EnvConfig {
|
|
172
|
-
const config: EnvConfig = {};
|
|
173
|
-
|
|
174
|
-
const healthTimeoutEnv = process.env['PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS'];
|
|
175
|
-
if (healthTimeoutEnv !== undefined) {
|
|
176
|
-
const parsed = parseInt(healthTimeoutEnv, 10);
|
|
177
|
-
if (!isNaN(parsed)) {
|
|
178
|
-
config.healthTimeout = parsed;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const imageTagEnv = process.env['SEARXNG_IMAGE_TAG']?.trim();
|
|
183
|
-
if (imageTagEnv) {
|
|
184
|
-
config.imageTag = imageTagEnv;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return config;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Sleep for ms milliseconds
|
|
192
|
-
*/
|
|
193
|
-
function sleep(ms: number): Promise<void> {
|
|
194
|
-
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Check if a TCP port is accepting connections
|
|
199
|
-
*/
|
|
200
|
-
function isPortListening(port: number, host: string = '127.0.0.1'): Promise<boolean> {
|
|
201
|
-
return new Promise<boolean>((resolve) => {
|
|
202
|
-
const socket: net.Socket = new net.Socket();
|
|
203
|
-
|
|
204
|
-
socket.setTimeout(2000); // 2 second timeout
|
|
205
|
-
|
|
206
|
-
socket.on('connect', () => {
|
|
207
|
-
socket.destroy();
|
|
208
|
-
resolve(true);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
socket.on('timeout', () => {
|
|
212
|
-
socket.destroy();
|
|
213
|
-
resolve(false);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
socket.on('error', () => {
|
|
217
|
-
socket.destroy();
|
|
218
|
-
resolve(false);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
socket.connect(port, host);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* SearxngManagerConfig type
|
|
227
|
-
*/
|
|
228
|
-
export interface SearxngManagerConfig {
|
|
229
|
-
port?: number;
|
|
230
|
-
settingsPath?: string; // Custom settings file path (for Tor mode)
|
|
231
|
-
imageName?: string;
|
|
232
|
-
imageTag?: string;
|
|
233
|
-
healthTimeout?: number;
|
|
234
|
-
pruneOldImages?: boolean;
|
|
235
|
-
containerName?: string;
|
|
236
|
-
stateDir?: string;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* SearxngContainerInfo type
|
|
241
|
-
*/
|
|
242
|
-
export interface SearxngContainerInfo {
|
|
243
|
-
id: string;
|
|
244
|
-
name: string;
|
|
245
|
-
port: number;
|
|
246
|
-
url: string;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* SearxngStatus type
|
|
251
|
-
*/
|
|
252
|
-
export interface SearxngStatus {
|
|
253
|
-
dockerAvailable: boolean;
|
|
254
|
-
healthy: boolean;
|
|
255
|
-
containerId?: string;
|
|
256
|
-
containerName?: string;
|
|
257
|
-
port?: number;
|
|
258
|
-
url?: string;
|
|
259
|
-
error?: string;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* DockerSearxngManager Class
|
|
264
|
-
*
|
|
265
|
-
* Simplified SearXNG Docker container manager.
|
|
266
|
-
* Session-scoped lifecycle with deterministic container naming.
|
|
267
|
-
* Also manages Docker images (pulls latest, prunes old images).
|
|
268
|
-
*/
|
|
269
|
-
/**
|
|
270
|
-
* Verify Docker is installed and daemon is responsive
|
|
271
|
-
*/
|
|
272
|
-
export async function verifyDockerInstalled(): Promise<{ installed: boolean; running: boolean; error?: string }> {
|
|
273
|
-
let lastError = 'Docker daemon is not running. Please start Docker.';
|
|
274
|
-
|
|
275
|
-
for (const candidate of getDockerConnectionCandidates()) {
|
|
276
|
-
try {
|
|
277
|
-
const docker = new Docker(candidate);
|
|
278
|
-
await docker.ping();
|
|
279
|
-
return { installed: true, running: true };
|
|
280
|
-
} catch (error) {
|
|
281
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (
|
|
286
|
-
lastError.includes('ENOENT') ||
|
|
287
|
-
lastError.includes('ECONNREFUSED') ||
|
|
288
|
-
lastError.includes('The system cannot find the file specified') ||
|
|
289
|
-
lastError.includes('EACCES') ||
|
|
290
|
-
lastError.includes('permission')
|
|
291
|
-
) {
|
|
292
|
-
let errorMsg = 'Docker daemon is not running or inaccessible.';
|
|
293
|
-
if (process.platform === 'linux') {
|
|
294
|
-
errorMsg += ' On Linux, run: sudo systemctl start docker && sudo usermod -aG docker $USER. Then log out and back in.';
|
|
295
|
-
} else if (process.platform === 'darwin') {
|
|
296
|
-
errorMsg += ' Start Docker Desktop from Applications.';
|
|
297
|
-
} else if (process.platform === 'win32') {
|
|
298
|
-
errorMsg += ' Start Docker Desktop from the Start menu.';
|
|
299
|
-
}
|
|
300
|
-
return { installed: true, running: false, error: errorMsg };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return { installed: false, running: false, error: `Docker not found or inaccessible: ${lastError}` };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export class DockerSearxngManager {
|
|
307
|
-
private readonly docker: Docker | null = null;
|
|
308
|
-
private container: Docker.Container | null = null;
|
|
309
|
-
private containerInfo: SearxngContainerInfo | null = null;
|
|
310
|
-
private readonly config: Required<SearxngManagerConfig>;
|
|
311
|
-
private ctx: ExtensionContext | null = null;
|
|
312
|
-
private sessionId: string | null = null;
|
|
313
|
-
private starting: Promise<void> | null = null;
|
|
314
|
-
private readonly stateManager: StateManager;
|
|
315
|
-
private readonly _extensionDir: string;
|
|
316
|
-
private readonly _settingsPath: string;
|
|
317
|
-
private containerStartupLock: Promise<void> | null = null;
|
|
318
|
-
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
319
|
-
private heartbeatRunning = false;
|
|
320
|
-
|
|
321
|
-
constructor(
|
|
322
|
-
_extensionDir: string,
|
|
323
|
-
config: SearxngManagerConfig = {},
|
|
324
|
-
options?: { docker?: Docker },
|
|
325
|
-
) {
|
|
326
|
-
this.config = {
|
|
327
|
-
...DEFAULT_CONFIG,
|
|
328
|
-
...loadConfigFromEnv(),
|
|
329
|
-
...config,
|
|
330
|
-
} as Required<SearxngManagerConfig>;
|
|
331
|
-
this._extensionDir = _extensionDir;
|
|
332
|
-
this._settingsPath = config.settingsPath || path.join(_extensionDir, 'config', 'searxng', 'default-settings.yml');
|
|
333
|
-
// Initialize Docker client
|
|
334
|
-
try {
|
|
335
|
-
this.docker = options?.docker || new Docker(getDockerConnectionCandidates()[0]);
|
|
336
|
-
} catch (error) {
|
|
337
|
-
logger.warn('[SearXNG Manager] Failed to initialize Docker client:', error);
|
|
338
|
-
this.docker = null;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Initialize StateManager for singleton mode
|
|
342
|
-
this.stateManager = new StateManager(config.stateDir);
|
|
343
|
-
// Cleanup is orchestrated by the extension lifecycle hook in index.ts, not by process signals here.
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Set extension context for UI notifications
|
|
348
|
-
*/
|
|
349
|
-
setContext(ctx: ExtensionContext): void {
|
|
350
|
-
this.ctx = ctx;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Notify user via UI
|
|
355
|
-
*/
|
|
356
|
-
private notify(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info'): void {
|
|
357
|
-
if (this.ctx?.ui) {
|
|
358
|
-
// Convert 'success' to 'info' for UI compatibility
|
|
359
|
-
const uiType: 'info' | 'warning' | 'error' = type === 'success' ? 'info' : type;
|
|
360
|
-
this.ctx.ui.notify(message, uiType);
|
|
361
|
-
} else {
|
|
362
|
-
logger.log(`[SearXNG Manager ${type.toUpperCase()}] ${message}`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Get StateManager instance
|
|
368
|
-
*/
|
|
369
|
-
getStateManager(): StateManager | null {
|
|
370
|
-
return this.stateManager;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Check if heartbeat is running
|
|
375
|
-
*/
|
|
376
|
-
isHeartbeatRunning(): boolean {
|
|
377
|
-
return this.heartbeatRunning;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Cleanup stale sessions — removes sessions whose process is dead OR that haven't
|
|
382
|
-
* been seen in over 2 hours. Uses StateManager's built-in cleanup which checks
|
|
383
|
-
* both the timeout threshold AND process liveness (so crashed processes are
|
|
384
|
-
* removed immediately without waiting for the timeout to expire).
|
|
385
|
-
*/
|
|
386
|
-
async cleanupStaleSessions(): Promise<void> {
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
const removed = await this.stateManager.cleanupStaleSessions(2 * 60 * 60 * 1000); // 2 hours
|
|
390
|
-
if (removed > 0) {
|
|
391
|
-
logger.log(`[SearXNG Manager] Cleaned up ${removed} stale session(s)`);
|
|
392
|
-
}
|
|
393
|
-
} catch (error) {
|
|
394
|
-
logger.error('[SearXNG Manager] Error cleaning up stale sessions:', error);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Verify container health
|
|
400
|
-
*/
|
|
401
|
-
async verifyContainerHealthy(): Promise<boolean> {
|
|
402
|
-
if (!this.container || this.docker === null) {
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
try {
|
|
407
|
-
const info: DockerContainerInspectInfo = await this.container.inspect() as DockerContainerInspectInfo;
|
|
408
|
-
const portListening = await isPortListening(this.config.port);
|
|
409
|
-
return info.State.Running && portListening;
|
|
410
|
-
} catch {
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Start heartbeat monitoring
|
|
417
|
-
*/
|
|
418
|
-
startHeartbeat(): void {
|
|
419
|
-
if (this.heartbeatRunning) {
|
|
420
|
-
logger.warn('[SearXNG Manager] Heartbeat already running');
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
this.heartbeatRunning = true;
|
|
425
|
-
const baseInterval = 30000; // 30 seconds
|
|
426
|
-
const jitter = Math.floor(Math.random() * 5000); // 0-5 seconds random
|
|
427
|
-
const interval = baseInterval + jitter;
|
|
428
|
-
|
|
429
|
-
logger.log(`[SearXNG Manager] Heartbeat interval: ${interval}ms (${baseInterval}ms base + ${jitter}ms jitter)`);
|
|
430
|
-
|
|
431
|
-
this.heartbeatInterval = setInterval(() => {
|
|
432
|
-
void (async (): Promise<void> => {
|
|
433
|
-
try {
|
|
434
|
-
// Refresh this session's lastSeen so it isn't pruned by cleanupStaleSessions
|
|
435
|
-
if (this.sessionId !== null) {
|
|
436
|
-
this.stateManager.updateHeartbeat(this.sessionId).catch(() => {});
|
|
437
|
-
}
|
|
438
|
-
// Remove sessions whose pi processes have died since the last cycle
|
|
439
|
-
await this.cleanupStaleSessions();
|
|
440
|
-
await this.verifyContainerHealthy();
|
|
441
|
-
} catch (error) {
|
|
442
|
-
logger.error('[SearXNG Manager] Heartbeat check failed:', error);
|
|
443
|
-
}
|
|
444
|
-
})();
|
|
445
|
-
}, interval).unref(); // Don't keep event loop alive when nothing else is running
|
|
446
|
-
|
|
447
|
-
logger.log('[SearXNG Manager] Heartbeat started');
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Stop heartbeat monitoring
|
|
452
|
-
*/
|
|
453
|
-
stopHeartbeat(): void {
|
|
454
|
-
this.heartbeatRunning = false;
|
|
455
|
-
|
|
456
|
-
if (this.heartbeatInterval) {
|
|
457
|
-
clearInterval(this.heartbeatInterval);
|
|
458
|
-
this.heartbeatInterval = null;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
logger.log('[SearXNG Manager] Heartbeat stopped');
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Acquire container for session (singleton mode)
|
|
466
|
-
*/
|
|
467
|
-
async acquire(sessionId: string): Promise<void> {
|
|
468
|
-
|
|
469
|
-
if (this.containerStartupLock) {
|
|
470
|
-
await this.containerStartupLock;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const existingSession = await this.stateManager.getSession(sessionId);
|
|
474
|
-
if (existingSession) {
|
|
475
|
-
logger.log(`[SearXNG Manager] Session ${sessionId} already has container reference`);
|
|
476
|
-
await this.stateManager.updateActivity(sessionId);
|
|
477
|
-
this.sessionId = sessionId;
|
|
478
|
-
// If this manager instance has no container reference (e.g. after a session
|
|
479
|
-
// reconnect where pi reuses the same session ID), attach to the running container
|
|
480
|
-
// so ensureReady() and getStatus() work correctly.
|
|
481
|
-
if (!this.container) {
|
|
482
|
-
try {
|
|
483
|
-
const existing = await this.findContainerByName(this.config.containerName);
|
|
484
|
-
if (existing) {
|
|
485
|
-
const info: DockerContainerInspectInfo = await existing.inspect() as DockerContainerInspectInfo;
|
|
486
|
-
this.container = existing;
|
|
487
|
-
|
|
488
|
-
const portBindings = info.NetworkSettings.Ports?.['8080/tcp'];
|
|
489
|
-
let port = this.config.port;
|
|
490
|
-
if (portBindings && portBindings.length > 0) {
|
|
491
|
-
const hostPort = portBindings[0]?.HostPort;
|
|
492
|
-
if (hostPort) {
|
|
493
|
-
port = parseInt(hostPort, 10);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
this.containerInfo = {
|
|
498
|
-
id: existing.id,
|
|
499
|
-
name: this.config.containerName,
|
|
500
|
-
port,
|
|
501
|
-
url: `http://localhost:${port}`,
|
|
502
|
-
};
|
|
503
|
-
this.config.port = port;
|
|
504
|
-
}
|
|
505
|
-
} catch (err) {
|
|
506
|
-
logger.warn('[SearXNG Manager] Could not attach to existing container on reconnect:', err);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
this.containerStartupLock = (async (): Promise<void> => {
|
|
513
|
-
try {
|
|
514
|
-
const containerName = this.config.containerName;
|
|
515
|
-
logger.log(`[SearXNG Manager] Acquiring container for session ${sessionId}`);
|
|
516
|
-
|
|
517
|
-
const existingContainer = await this.findContainerByName(containerName);
|
|
518
|
-
|
|
519
|
-
if (existingContainer) {
|
|
520
|
-
logger.log(`[SearXNG Manager] Reusing existing singleton container: ${containerName}`);
|
|
521
|
-
|
|
522
|
-
const info: DockerContainerInspectInfo = await existingContainer.inspect() as DockerContainerInspectInfo;
|
|
523
|
-
this.container = existingContainer;
|
|
524
|
-
|
|
525
|
-
let port = this.config.port;
|
|
526
|
-
const portBindings = info.NetworkSettings.Ports?.['8080/tcp'];
|
|
527
|
-
if (portBindings && portBindings.length > 0) {
|
|
528
|
-
const hostPort = portBindings[0]?.HostPort;
|
|
529
|
-
if (hostPort) {
|
|
530
|
-
port = parseInt(hostPort, 10);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
this.containerInfo = {
|
|
535
|
-
id: existingContainer.id,
|
|
536
|
-
name: containerName,
|
|
537
|
-
port,
|
|
538
|
-
url: `http://localhost:${port}`,
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
this.config.port = port;
|
|
542
|
-
|
|
543
|
-
if (!info.State.Running) {
|
|
544
|
-
await this.startContainer(existingContainer);
|
|
545
|
-
await this.waitForHealthy();
|
|
546
|
-
} else {
|
|
547
|
-
const healthy = await this.verifyContainerHealthy();
|
|
548
|
-
if (!healthy) {
|
|
549
|
-
await this.waitForHealthy();
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
} else {
|
|
553
|
-
await this.startSingleton();
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
await this.stateManager.addSession(sessionId, containerName);
|
|
557
|
-
this.sessionId = sessionId;
|
|
558
|
-
logger.log(`[SearXNG Manager] Session ${sessionId} acquired container reference`);
|
|
559
|
-
|
|
560
|
-
} finally {
|
|
561
|
-
this.containerStartupLock = null;
|
|
562
|
-
}
|
|
563
|
-
})();
|
|
564
|
-
|
|
565
|
-
await this.containerStartupLock;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Release container for session (singleton mode)
|
|
570
|
-
*/
|
|
571
|
-
async release(sessionId: string): Promise<void> {
|
|
572
|
-
|
|
573
|
-
logger.log(`[SearXNG Manager] Releasing container for session ${sessionId}`);
|
|
574
|
-
|
|
575
|
-
await this.stateManager.removeSession(sessionId);
|
|
576
|
-
|
|
577
|
-
// Clean up sessions whose processes have died before counting remaining references.
|
|
578
|
-
// Without this, stale sessions from crashed/killed terminals accumulate in the state
|
|
579
|
-
// file and prevent the container from stopping even when no real sessions remain.
|
|
580
|
-
await this.cleanupStaleSessions();
|
|
581
|
-
|
|
582
|
-
const remainingSessions = await this.stateManager.getAllSessions();
|
|
583
|
-
if (Object.keys(remainingSessions).length === 0) {
|
|
584
|
-
logger.log('[SearXNG Manager] No more sessions, stopping singleton container');
|
|
585
|
-
await this.stopSingleton();
|
|
586
|
-
} else {
|
|
587
|
-
logger.log(`[SearXNG Manager] Container still in use by ${Object.keys(remainingSessions).length} session(s)`);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Start singleton container
|
|
593
|
-
*/
|
|
594
|
-
private async startSingleton(): Promise<void> {
|
|
595
|
-
const containerName = this.config.containerName;
|
|
596
|
-
|
|
597
|
-
logger.log(`[SearXNG Manager] Starting singleton container: ${containerName}`);
|
|
598
|
-
|
|
599
|
-
await this.ensureImage();
|
|
600
|
-
|
|
601
|
-
try {
|
|
602
|
-
const containerResult = await this.createContainer(containerName, this.config.port);
|
|
603
|
-
this.container = containerResult.container;
|
|
604
|
-
const assignedPort = containerResult.assignedPort;
|
|
605
|
-
|
|
606
|
-
await this.startContainer(this.container);
|
|
607
|
-
|
|
608
|
-
this.containerInfo = {
|
|
609
|
-
id: this.container.id,
|
|
610
|
-
name: containerName,
|
|
611
|
-
port: assignedPort,
|
|
612
|
-
url: `http://localhost:${assignedPort}`,
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
this.config.port = assignedPort;
|
|
616
|
-
|
|
617
|
-
await this.waitForHealthy();
|
|
618
|
-
|
|
619
|
-
logger.log(`[SearXNG Manager] Singleton container started on port ${assignedPort}`);
|
|
620
|
-
} catch (error) {
|
|
621
|
-
logger.error('[SearXNG Manager] Failed to start singleton container:', error);
|
|
622
|
-
throw error;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Stop singleton container
|
|
628
|
-
*/
|
|
629
|
-
private async stopSingleton(): Promise<void> {
|
|
630
|
-
logger.log('[SearXNG Manager] Stopping singleton container');
|
|
631
|
-
|
|
632
|
-
if (this.heartbeatInterval) {
|
|
633
|
-
this.stopHeartbeat();
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (this.container) {
|
|
637
|
-
try {
|
|
638
|
-
await this.stopContainer(this.container);
|
|
639
|
-
await this.removeContainer(this.container);
|
|
640
|
-
logger.log('[SearXNG Manager] Singleton container stopped');
|
|
641
|
-
} catch (error) {
|
|
642
|
-
logger.warn('[SearXNG Manager] Error stopping singleton container:', error);
|
|
643
|
-
} finally {
|
|
644
|
-
this.container = null;
|
|
645
|
-
this.containerInfo = null;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Check if Docker is available
|
|
652
|
-
*/
|
|
653
|
-
private async checkDocker(): Promise<boolean> {
|
|
654
|
-
if (!this.docker) {
|
|
655
|
-
return false;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
try {
|
|
659
|
-
await this.docker.ping();
|
|
660
|
-
return true;
|
|
661
|
-
} catch {
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* Get full image name with tag
|
|
668
|
-
*/
|
|
669
|
-
private getFullImageName(): string {
|
|
670
|
-
return `${this.config.imageName}:${this.config.imageTag}`;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Check if Docker image exists locally
|
|
675
|
-
*/
|
|
676
|
-
private async imageExists(): Promise<boolean> {
|
|
677
|
-
if (!this.docker) {
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
try {
|
|
682
|
-
const fullImage = this.getFullImageName();
|
|
683
|
-
const image = this.docker.getImage(fullImage);
|
|
684
|
-
await image.inspect();
|
|
685
|
-
return true;
|
|
686
|
-
} catch {
|
|
687
|
-
// Image doesn't exist
|
|
688
|
-
return false;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Pull the latest Docker image
|
|
694
|
-
*/
|
|
695
|
-
private async pullLatestImage(): Promise<void> {
|
|
696
|
-
if (this.docker === null) {
|
|
697
|
-
throw new Error('Docker not available.');
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const fullImage = this.getFullImageName();
|
|
701
|
-
|
|
702
|
-
this.notify(`Updating SearXNG image: ${fullImage}...`, 'info');
|
|
703
|
-
this.notify('This may take 1-2 minutes on first run.', 'warning');
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
const docker = this.docker;
|
|
707
|
-
await new Promise<void>((resolve, reject) => {
|
|
708
|
-
void docker.pull(fullImage, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
|
709
|
-
if (err) {
|
|
710
|
-
reject(err);
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
docker.modem.followProgress(stream, (err: Error | null) => {
|
|
715
|
-
if (err) {
|
|
716
|
-
reject(err);
|
|
717
|
-
} else {
|
|
718
|
-
resolve();
|
|
719
|
-
}
|
|
720
|
-
}, (event: Record<string, unknown>) => {
|
|
721
|
-
// Optional: log progress events
|
|
722
|
-
if (event['status'] === 'Pulling fs layer' || event['status'] === 'Downloading') {
|
|
723
|
-
// Could show progress here
|
|
724
|
-
}
|
|
725
|
-
});
|
|
726
|
-
});
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
this.notify('SearXNG image updated successfully.', 'success');
|
|
730
|
-
} catch (error: unknown) {
|
|
731
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
732
|
-
throw new Error(`Failed to pull image: ${errMsg}`, { cause: error });
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Prune old SearXNG images (dangling and non-latest tags)
|
|
738
|
-
*/
|
|
739
|
-
private async pruneOldImages(): Promise<void> {
|
|
740
|
-
if (!this.docker) {
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (!this.config.pruneOldImages) {
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
try {
|
|
749
|
-
this.notify('Cleaning up old SearXNG images...', 'info');
|
|
750
|
-
|
|
751
|
-
// Get all images for the repository
|
|
752
|
-
const images: DockerImageInfo[] = await this.docker.listImages({ filters: { reference: [this.config.imageName] } });
|
|
753
|
-
|
|
754
|
-
const currentImage = this.getFullImageName();
|
|
755
|
-
|
|
756
|
-
for (const image of images) {
|
|
757
|
-
const repoTags = image.RepoTags;
|
|
758
|
-
if (repoTags && repoTags.length > 0) {
|
|
759
|
-
for (const tag of repoTags) {
|
|
760
|
-
// Skip the current image (latest)
|
|
761
|
-
if (tag === currentImage) {
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// Remove old images
|
|
766
|
-
const img = this.docker.getImage(tag);
|
|
767
|
-
try {
|
|
768
|
-
await img.remove({ force: false });
|
|
769
|
-
this.notify(`Removed old image: ${tag}`, 'info');
|
|
770
|
-
} catch (error) {
|
|
771
|
-
// Image might be in use, that's okay
|
|
772
|
-
logger.warn('[SearXNG Manager] Could not remove image:', tag, error);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Also prune dangling images
|
|
779
|
-
await this.docker.pruneImages({ filters: { dangling: ['true'] } });
|
|
780
|
-
|
|
781
|
-
this.notify('Image cleanup complete.', 'success');
|
|
782
|
-
} catch (error) {
|
|
783
|
-
logger.warn('[SearXNG Manager] Error pruning old images:', error);
|
|
784
|
-
// Don't fail the whole process if pruning fails
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* Ensure image is available and up-to-date
|
|
790
|
-
*/
|
|
791
|
-
private async ensureImage(): Promise<void> {
|
|
792
|
-
const imageExists = await this.imageExists();
|
|
793
|
-
|
|
794
|
-
if (!imageExists) {
|
|
795
|
-
// Image doesn't exist, pull it
|
|
796
|
-
await this.pullLatestImage();
|
|
797
|
-
} else {
|
|
798
|
-
// Image exists, prune old ones and optionally pull latest
|
|
799
|
-
await this.pruneOldImages();
|
|
800
|
-
|
|
801
|
-
// Optionally: always pull latest to stay up-to-date
|
|
802
|
-
// Uncomment the next line to always update on start:
|
|
803
|
-
// await this.pullLatestImage();
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Find container by name
|
|
809
|
-
*/
|
|
810
|
-
private async findContainerByName(name: string): Promise<Docker.Container | null> {
|
|
811
|
-
if (!this.docker) {
|
|
812
|
-
return null;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
try {
|
|
816
|
-
const containers: DockerContainerListInfo[] = await this.docker.listContainers({ all: true });
|
|
817
|
-
|
|
818
|
-
for (const c of containers) {
|
|
819
|
-
if (c.Names.some((n: string) => n === `/${name}`)) {
|
|
820
|
-
return this.docker.getContainer(c.Id);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
return null;
|
|
825
|
-
} catch {
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Create a new container with port auto-assignment
|
|
832
|
-
*/
|
|
833
|
-
private async createContainer(
|
|
834
|
-
name: string,
|
|
835
|
-
preferredPort: number | null,
|
|
836
|
-
): Promise<{ container: Docker.Container; assignedPort: number }> {
|
|
837
|
-
if (!this.docker) {
|
|
838
|
-
throw new Error('Docker not available.');
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const fullImage = this.getFullImageName();
|
|
842
|
-
|
|
843
|
-
// Mount both settings.yml and limiter.toml for complete bot detection disable
|
|
844
|
-
const settingsPath = this._settingsPath;
|
|
845
|
-
const limiterConfigPath = path.join(this._extensionDir, 'config', 'searxng', 'limiter.toml');
|
|
846
|
-
|
|
847
|
-
// Verify configuration files exist
|
|
848
|
-
if (!fs.existsSync(settingsPath)) {
|
|
849
|
-
throw new Error(`Settings file not found: ${settingsPath}.`);
|
|
850
|
-
}
|
|
851
|
-
if (!fs.existsSync(limiterConfigPath)) {
|
|
852
|
-
throw new Error(`Limiter config not found: ${limiterConfigPath}. Please ensure limiter.toml exists.`);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Try preferred port first, or use auto-assignment (empty string)
|
|
856
|
-
const hostPort = preferredPort ? preferredPort.toString() : '';
|
|
857
|
-
|
|
858
|
-
const containerConfig: DockerContainerCreateOptions = {
|
|
859
|
-
name,
|
|
860
|
-
Image: fullImage,
|
|
861
|
-
ExposedPorts: { '8080/tcp': {} },
|
|
862
|
-
HostConfig: {
|
|
863
|
-
PortBindings: {
|
|
864
|
-
'8080/tcp': [{ HostPort: hostPort }],
|
|
865
|
-
},
|
|
866
|
-
Binds: [
|
|
867
|
-
`${toDockerBindPath(settingsPath)}:/etc/searxng/settings.yml:ro`,
|
|
868
|
-
`${toDockerBindPath(limiterConfigPath)}:/etc/searxng/limiter.toml:ro`,
|
|
869
|
-
],
|
|
870
|
-
AutoRemove: false,
|
|
871
|
-
ExtraHosts: getDockerHostGatewayExtraHosts(),
|
|
872
|
-
},
|
|
873
|
-
Env: [
|
|
874
|
-
// No SEARXNG_LIMITER or SEARXNG_BIND_ADDRESS - use mounted config files only
|
|
875
|
-
],
|
|
876
|
-
Labels: {
|
|
877
|
-
'managed-by': 'pi-research',
|
|
878
|
-
'mode': 'singleton',
|
|
879
|
-
},
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
const container = await this.docker.createContainer(containerConfig);
|
|
883
|
-
|
|
884
|
-
// Query the actual assigned port (important for auto-assignment)
|
|
885
|
-
const info: DockerContainerInspectInfo = await container.inspect() as DockerContainerInspectInfo;
|
|
886
|
-
const portBindings = info.NetworkSettings.Ports?.['8080/tcp'];
|
|
887
|
-
const assignedPort = (portBindings && portBindings.length > 0 && portBindings[0]?.HostPort)
|
|
888
|
-
? parseInt(portBindings[0].HostPort, 10)
|
|
889
|
-
: (preferredPort ?? 0);
|
|
890
|
-
|
|
891
|
-
logger.log(`[SearXNG Manager] Container created with port ${assignedPort}`);
|
|
892
|
-
|
|
893
|
-
return { container, assignedPort };
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Start a container
|
|
898
|
-
*/
|
|
899
|
-
private async startContainer(container: Docker.Container): Promise<void> {
|
|
900
|
-
await container.start();
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Stop a container
|
|
905
|
-
*/
|
|
906
|
-
private async stopContainer(container: Docker.Container): Promise<void> {
|
|
907
|
-
try {
|
|
908
|
-
await container.stop({ t: 10 });
|
|
909
|
-
} catch (error: unknown) {
|
|
910
|
-
// Container might already be stopped (error 304)
|
|
911
|
-
const errorStr = String(error);
|
|
912
|
-
if (errorStr.includes('is not running') ||
|
|
913
|
-
errorStr.includes('container already stopped') ||
|
|
914
|
-
errorStr.includes('304')) {
|
|
915
|
-
// Already stopped, that's fine
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
logger.warn('[SearXNG Manager] Error stopping container:', error);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* Remove a container
|
|
924
|
-
*/
|
|
925
|
-
private async removeContainer(container: Docker.Container): Promise<void> {
|
|
926
|
-
await Promise.race([
|
|
927
|
-
container.remove({ v: true, force: true }),
|
|
928
|
-
new Promise<void>((_, reject) => {
|
|
929
|
-
setTimeout(() => {
|
|
930
|
-
reject(new Error('container.remove timed out after 15s'));
|
|
931
|
-
}, 15000);
|
|
932
|
-
}),
|
|
933
|
-
]).catch((err: unknown) => {
|
|
934
|
-
if (!String(err).includes('No such container')) {
|
|
935
|
-
logger.warn('[SearXNG Manager] Error removing container:', err);
|
|
936
|
-
}
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* Wait for container to be healthy
|
|
942
|
-
*/
|
|
943
|
-
private async waitForHealthy(): Promise<void> {
|
|
944
|
-
if (!this.container) {
|
|
945
|
-
throw new Error('No container to wait for.');
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
const startTime = Date.now();
|
|
949
|
-
const timeout = this.config.healthTimeout;
|
|
950
|
-
const checkInterval = 3000; // Check every 3 seconds
|
|
951
|
-
const STARTUP_DELAY = 5000; // Wait 5 seconds for initial startup
|
|
952
|
-
|
|
953
|
-
this.notify('Starting SearXNG container and waiting for it to be ready...', 'info');
|
|
954
|
-
this.notify('If this is your first run, it may take 1-2 minutes to download the image.', 'warning');
|
|
955
|
-
this.notify('Subsequent runs will be much faster.', 'info');
|
|
956
|
-
|
|
957
|
-
// Wait for container to start (initial delay)
|
|
958
|
-
await sleep(STARTUP_DELAY);
|
|
959
|
-
|
|
960
|
-
let consecutiveHealthyChecks = 0;
|
|
961
|
-
const HEALTHY_CHECKS_NEEDED = 2; // Need 2 consecutive healthy checks
|
|
962
|
-
let lastElapsedTime = '';
|
|
963
|
-
|
|
964
|
-
while (Date.now() - startTime < timeout) {
|
|
965
|
-
try {
|
|
966
|
-
// Check if container still exists and get its state in one call
|
|
967
|
-
let info: DockerContainerInspectInfo;
|
|
968
|
-
try {
|
|
969
|
-
info = await this.container.inspect() as DockerContainerInspectInfo;
|
|
970
|
-
} catch (error: unknown) {
|
|
971
|
-
const err = error as { statusCode?: number };
|
|
972
|
-
if (err.statusCode === 404) {
|
|
973
|
-
// Container was removed, recreate it
|
|
974
|
-
this.notify('Container was removed, recreating...', 'warning');
|
|
975
|
-
const goneError = new Error('CONTAINER_GONE');
|
|
976
|
-
goneError.cause = error;
|
|
977
|
-
throw goneError;
|
|
978
|
-
}
|
|
979
|
-
throw error;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// Check if container is still running
|
|
983
|
-
if (!info.State.Running) {
|
|
984
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
985
|
-
|
|
986
|
-
// Container has exited, get logs to understand why
|
|
987
|
-
try {
|
|
988
|
-
const logs = await this.container.logs({ stdout: true, stderr: true, tail: 50 });
|
|
989
|
-
const logsStr = logs.toString();
|
|
990
|
-
|
|
991
|
-
this.notify(`Container exited with status: ${info.State.Status} (${elapsed}s elapsed)`, 'error');
|
|
992
|
-
logger.error('[SearXNG Manager] Container logs:', logsStr);
|
|
993
|
-
|
|
994
|
-
// Check for common error patterns
|
|
995
|
-
if (logsStr.includes('bind') && (logsStr.includes('address already in use') || logsStr.includes('already allocated'))) {
|
|
996
|
-
throw new Error(`Failed to start container: Port ${this.config.port} is already in use. Check if another SearXNG container or service is using this port.`, { cause: logsStr });
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// Generic error with logs
|
|
1000
|
-
throw new Error(`Container exited unexpectedly. Last 50 lines of logs:\n${logsStr}`, { cause: logsStr });
|
|
1001
|
-
} catch (error: unknown) {
|
|
1002
|
-
if (error instanceof Error) {
|
|
1003
|
-
throw error; // Re-throw our custom error
|
|
1004
|
-
}
|
|
1005
|
-
// If log retrieval failed, throw a generic error
|
|
1006
|
-
throw new Error(`Container exited with status: ${info.State.Status}. Unable to retrieve logs.`, { cause: error });
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// Enhanced health check: container running AND port is listening
|
|
1011
|
-
// SearXNG bot detection blocks HTTP requests, so we use TCP check instead
|
|
1012
|
-
const portListening = await isPortListening(this.config.port);
|
|
1013
|
-
if (portListening) {
|
|
1014
|
-
consecutiveHealthyChecks++;
|
|
1015
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
1016
|
-
if (consecutiveHealthyChecks >= HEALTHY_CHECKS_NEEDED) {
|
|
1017
|
-
this.notify(`SearXNG container is ready! (${elapsed}s elapsed)`, 'success');
|
|
1018
|
-
return;
|
|
1019
|
-
} else if (lastElapsedTime !== elapsed) {
|
|
1020
|
-
this.notify(`SearXNG container starting up... (${elapsed}s elapsed)`, 'info');
|
|
1021
|
-
lastElapsedTime = elapsed;
|
|
1022
|
-
}
|
|
1023
|
-
} else {
|
|
1024
|
-
// Container is running but port not listening yet
|
|
1025
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
1026
|
-
if (lastElapsedTime !== elapsed) {
|
|
1027
|
-
this.notify(`SearXNG container running, waiting for port ${this.config.port} to be ready... (${elapsed}s elapsed)`, 'info');
|
|
1028
|
-
lastElapsedTime = elapsed;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
await sleep(checkInterval);
|
|
1033
|
-
} catch (error: unknown) {
|
|
1034
|
-
// If container is gone, rethrow to trigger recreation
|
|
1035
|
-
if (error instanceof Error && error['message'] === 'CONTAINER_GONE') {
|
|
1036
|
-
throw error;
|
|
1037
|
-
}
|
|
1038
|
-
// Log other errors but continue
|
|
1039
|
-
logger.error('[SearXNG Manager] Health check error:', error);
|
|
1040
|
-
await sleep(checkInterval);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
1045
|
-
const containerId = this.container.id;
|
|
1046
|
-
throw new Error(`Container did not become healthy within ${elapsed}s. Check Docker logs with: docker logs ${containerId}`);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
/**
|
|
1050
|
-
* Start SearXNG for a session
|
|
1051
|
-
*/
|
|
1052
|
-
async start(sessionId: string): Promise<void> {
|
|
1053
|
-
this.sessionId = sessionId;
|
|
1054
|
-
|
|
1055
|
-
const dockerAvailable = await this.checkDocker();
|
|
1056
|
-
|
|
1057
|
-
if (!dockerAvailable) {
|
|
1058
|
-
this.notify('Docker is not available. SearXNG search functionality will not work.', 'error');
|
|
1059
|
-
throw new Error('Docker not available');
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
logger.log('[SearXNG Manager] Starting in singleton mode');
|
|
1063
|
-
|
|
1064
|
-
await this.cleanupStaleSessions();
|
|
1065
|
-
await this.acquire(sessionId);
|
|
1066
|
-
this.startHeartbeat();
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* Stop SearXNG container
|
|
1071
|
-
*/
|
|
1072
|
-
async stop(): Promise<void> {
|
|
1073
|
-
if (this.sessionId) {
|
|
1074
|
-
logger.log(`[SearXNG Manager] Releasing container for session ${this.sessionId}`);
|
|
1075
|
-
await this.release(this.sessionId);
|
|
1076
|
-
this.sessionId = null;
|
|
1077
|
-
this.starting = null;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* Get current status
|
|
1083
|
-
*/
|
|
1084
|
-
async getStatus(): Promise<SearxngStatus> {
|
|
1085
|
-
const dockerAvailable = await this.checkDocker();
|
|
1086
|
-
|
|
1087
|
-
if (!dockerAvailable) {
|
|
1088
|
-
return {
|
|
1089
|
-
dockerAvailable: false,
|
|
1090
|
-
healthy: false,
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
if (this.container && this.containerInfo) {
|
|
1095
|
-
try {
|
|
1096
|
-
const info: DockerContainerInspectInfo = await this.container.inspect() as DockerContainerInspectInfo;
|
|
1097
|
-
|
|
1098
|
-
// Enhanced health check: container running AND port is listening
|
|
1099
|
-
// SearXNG bot detection blocks HTTP requests, so we use TCP check instead
|
|
1100
|
-
const portListening = await isPortListening(this.config.port);
|
|
1101
|
-
const healthy = info.State.Running && portListening;
|
|
1102
|
-
|
|
1103
|
-
return {
|
|
1104
|
-
containerId: this.container.id,
|
|
1105
|
-
containerName: this.containerInfo.name,
|
|
1106
|
-
port: this.containerInfo.port,
|
|
1107
|
-
url: this.containerInfo.url,
|
|
1108
|
-
healthy,
|
|
1109
|
-
dockerAvailable: true,
|
|
1110
|
-
};
|
|
1111
|
-
} catch (error) {
|
|
1112
|
-
return {
|
|
1113
|
-
dockerAvailable: true,
|
|
1114
|
-
healthy: false,
|
|
1115
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
return {
|
|
1121
|
-
dockerAvailable: true,
|
|
1122
|
-
healthy: false,
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
/**
|
|
1127
|
-
* Get SearXNG URL
|
|
1128
|
-
*/
|
|
1129
|
-
getSearxngUrl(): string {
|
|
1130
|
-
if (this.containerInfo) {
|
|
1131
|
-
return this.containerInfo.url;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
throw new Error('SearXNG container not running.');
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
/**
|
|
1138
|
-
* Ensure container is ready before use
|
|
1139
|
-
*/
|
|
1140
|
-
async ensureReady(): Promise<void> {
|
|
1141
|
-
if (!this.container || !this.containerInfo) {
|
|
1142
|
-
throw new Error('SearXNG container not initialized. Call acquire() first.');
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
// If a start is already in progress, wait for it
|
|
1146
|
-
if (this.starting) {
|
|
1147
|
-
await this.starting;
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// Check if container is healthy
|
|
1152
|
-
const status = await this.getStatus();
|
|
1153
|
-
if (!status.healthy) {
|
|
1154
|
-
// Directly stop/remove/restart the container rather than going through start() →
|
|
1155
|
-
// acquire(). acquire() early-returns when the session already exists in state, so
|
|
1156
|
-
// calling start() here would leave the unhealthy container in place.
|
|
1157
|
-
this.starting = (async (): Promise<void> => {
|
|
1158
|
-
try {
|
|
1159
|
-
if (this.container) {
|
|
1160
|
-
try {
|
|
1161
|
-
await this.stopContainer(this.container);
|
|
1162
|
-
} catch { /* ignore — may already be stopped */ }
|
|
1163
|
-
try {
|
|
1164
|
-
await this.removeContainer(this.container);
|
|
1165
|
-
} catch { /* ignore — may already be gone */ }
|
|
1166
|
-
this.container = null;
|
|
1167
|
-
this.containerInfo = null;
|
|
1168
|
-
}
|
|
1169
|
-
await this.startSingleton();
|
|
1170
|
-
} finally {
|
|
1171
|
-
this.starting = null;
|
|
1172
|
-
}
|
|
1173
|
-
})();
|
|
1174
|
-
|
|
1175
|
-
try {
|
|
1176
|
-
await this.starting;
|
|
1177
|
-
} catch (error: unknown) {
|
|
1178
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1179
|
-
throw new Error(`SearXNG container is not healthy and restart failed: ${errMsg}`, { cause: error });
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
}
|