@outfitter/daemon 0.2.0 → 0.2.2
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/dist/errors.d.ts +2 -0
- package/dist/errors.js +15 -0
- package/dist/health.d.ts +2 -0
- package/dist/health.js +7 -0
- package/dist/ipc.d.ts +2 -0
- package/dist/ipc.js +9 -0
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.js +8 -0
- package/dist/locking.d.ts +3 -0
- package/dist/locking.js +16 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +15 -0
- package/dist/shared/@outfitter/daemon-3j14csts.js +180 -0
- package/dist/shared/@outfitter/daemon-3qezw7hc.d.ts +154 -0
- package/dist/shared/@outfitter/daemon-5as2p88e.d.ts +69 -0
- package/dist/shared/@outfitter/daemon-6efpehdg.d.ts +133 -0
- package/dist/shared/@outfitter/daemon-8tpctdgw.d.ts +127 -0
- package/dist/shared/@outfitter/daemon-906e24jc.js +106 -0
- package/dist/shared/@outfitter/daemon-9w2ey87r.d.ts +161 -0
- package/dist/shared/@outfitter/daemon-bd6kcdnj.d.ts +32 -0
- package/dist/shared/@outfitter/daemon-c1zbfqq5.js +50 -0
- package/dist/shared/@outfitter/daemon-cy5wntm2.js +207 -0
- package/dist/shared/@outfitter/daemon-dzt3fqvp.js +9 -0
- package/dist/shared/@outfitter/daemon-h536nv4k.d.ts +103 -0
- package/dist/shared/@outfitter/daemon-qqn2jpsg.js +25 -0
- package/dist/shared/@outfitter/daemon-wz4peqjh.js +48 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +7 -0
- package/package.json +47 -5
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { ConnectionRefusedError, ConnectionTimeoutError, DaemonConnectionError, LockError, ProtocolError, StaleSocketError } from "./shared/@outfitter/daemon-8tpctdgw";
|
|
2
|
+
export { StaleSocketError, ProtocolError, LockError, DaemonConnectionError, ConnectionTimeoutError, ConnectionRefusedError };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ConnectionRefusedError,
|
|
4
|
+
ConnectionTimeoutError,
|
|
5
|
+
LockError,
|
|
6
|
+
ProtocolError,
|
|
7
|
+
StaleSocketError
|
|
8
|
+
} from "./shared/@outfitter/daemon-qqn2jpsg.js";
|
|
9
|
+
export {
|
|
10
|
+
StaleSocketError,
|
|
11
|
+
ProtocolError,
|
|
12
|
+
LockError,
|
|
13
|
+
ConnectionTimeoutError,
|
|
14
|
+
ConnectionRefusedError
|
|
15
|
+
};
|
package/dist/health.d.ts
ADDED
package/dist/health.js
ADDED
package/dist/ipc.d.ts
ADDED
package/dist/ipc.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { LockHandle, acquireDaemonLock, isDaemonAlive, isProcessAlive, readLockPid, releaseDaemonLock } from "./shared/@outfitter/daemon-h536nv4k";
|
|
2
|
+
import "./shared/@outfitter/daemon-8tpctdgw";
|
|
3
|
+
export { releaseDaemonLock, readLockPid, isProcessAlive, isDaemonAlive, acquireDaemonLock, LockHandle };
|
package/dist/locking.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
acquireDaemonLock,
|
|
4
|
+
isDaemonAlive,
|
|
5
|
+
isProcessAlive,
|
|
6
|
+
readLockPid,
|
|
7
|
+
releaseDaemonLock
|
|
8
|
+
} from "./shared/@outfitter/daemon-906e24jc.js";
|
|
9
|
+
import"./shared/@outfitter/daemon-qqn2jpsg.js";
|
|
10
|
+
export {
|
|
11
|
+
releaseDaemonLock,
|
|
12
|
+
readLockPid,
|
|
13
|
+
isProcessAlive,
|
|
14
|
+
isDaemonAlive,
|
|
15
|
+
acquireDaemonLock
|
|
16
|
+
};
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
DaemonError
|
|
4
|
+
} from "./daemon-dzt3fqvp.js";
|
|
5
|
+
|
|
6
|
+
// packages/daemon/src/lifecycle.ts
|
|
7
|
+
import { mkdir, unlink, writeFile } from "fs/promises";
|
|
8
|
+
import { dirname } from "path";
|
|
9
|
+
import { Result } from "@outfitter/contracts";
|
|
10
|
+
function pidFileExists(path) {
|
|
11
|
+
return Bun.file(path).exists();
|
|
12
|
+
}
|
|
13
|
+
async function writePidFile(path, pid) {
|
|
14
|
+
try {
|
|
15
|
+
const dir = dirname(path);
|
|
16
|
+
await mkdir(dir, { recursive: true });
|
|
17
|
+
await writeFile(path, String(pid), { flag: "wx" });
|
|
18
|
+
return Result.ok(undefined);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return Result.err(new DaemonError({
|
|
21
|
+
code: "PID_ERROR",
|
|
22
|
+
message: `Failed to write PID file: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function removePidFile(path) {
|
|
27
|
+
try {
|
|
28
|
+
await unlink(path);
|
|
29
|
+
return Result.ok(undefined);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
32
|
+
return Result.ok(undefined);
|
|
33
|
+
}
|
|
34
|
+
return Result.err(new DaemonError({
|
|
35
|
+
code: "PID_ERROR",
|
|
36
|
+
message: `Failed to remove PID file: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function runShutdownHandlers(handlers, timeout, logger) {
|
|
41
|
+
const errors = [];
|
|
42
|
+
const runHandlers = async () => {
|
|
43
|
+
for (const handler of handlers) {
|
|
44
|
+
try {
|
|
45
|
+
await handler();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
48
|
+
errors.push(err);
|
|
49
|
+
logger?.warn("Shutdown handler failed", { error: err.message });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
54
|
+
setTimeout(() => resolve("timeout"), timeout);
|
|
55
|
+
});
|
|
56
|
+
const result = await Promise.race([
|
|
57
|
+
runHandlers().then(() => "completed"),
|
|
58
|
+
timeoutPromise
|
|
59
|
+
]);
|
|
60
|
+
return {
|
|
61
|
+
completed: result === "completed",
|
|
62
|
+
errors
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createDaemon(options) {
|
|
66
|
+
const internalState = {
|
|
67
|
+
state: "stopped",
|
|
68
|
+
options: {
|
|
69
|
+
name: options.name,
|
|
70
|
+
pidFile: options.pidFile,
|
|
71
|
+
logger: options.logger,
|
|
72
|
+
shutdownTimeout: options.shutdownTimeout ?? 5000
|
|
73
|
+
},
|
|
74
|
+
shutdownHandlers: [],
|
|
75
|
+
signalHandlers: {},
|
|
76
|
+
isShuttingDown: false
|
|
77
|
+
};
|
|
78
|
+
async function doStop() {
|
|
79
|
+
const { logger } = internalState.options;
|
|
80
|
+
if (internalState.isShuttingDown) {
|
|
81
|
+
return Result.ok(undefined);
|
|
82
|
+
}
|
|
83
|
+
if (internalState.state === "stopped") {
|
|
84
|
+
return Result.err(new DaemonError({
|
|
85
|
+
code: "NOT_RUNNING",
|
|
86
|
+
message: `Daemon "${internalState.options.name}" is not running`
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
internalState.isShuttingDown = true;
|
|
90
|
+
internalState.state = "stopping";
|
|
91
|
+
logger?.info("Daemon stopping", { name: internalState.options.name });
|
|
92
|
+
const { completed } = await runShutdownHandlers(internalState.shutdownHandlers, internalState.options.shutdownTimeout, logger);
|
|
93
|
+
if (!completed) {
|
|
94
|
+
logger?.warn("Shutdown handlers timed out", {
|
|
95
|
+
name: internalState.options.name,
|
|
96
|
+
timeout: internalState.options.shutdownTimeout
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (internalState.signalHandlers.sigterm) {
|
|
100
|
+
process.off("SIGTERM", internalState.signalHandlers.sigterm);
|
|
101
|
+
}
|
|
102
|
+
if (internalState.signalHandlers.sigint) {
|
|
103
|
+
process.off("SIGINT", internalState.signalHandlers.sigint);
|
|
104
|
+
}
|
|
105
|
+
const removeResult = await removePidFile(internalState.options.pidFile);
|
|
106
|
+
if (removeResult.isErr()) {
|
|
107
|
+
logger?.error("Failed to remove PID file", {
|
|
108
|
+
error: removeResult.error.message
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
internalState.state = "stopped";
|
|
112
|
+
internalState.isShuttingDown = false;
|
|
113
|
+
logger?.info("Daemon stopped", { name: internalState.options.name });
|
|
114
|
+
if (!completed) {
|
|
115
|
+
return Result.err(new DaemonError({
|
|
116
|
+
code: "SHUTDOWN_TIMEOUT",
|
|
117
|
+
message: `Shutdown handlers exceeded timeout of ${internalState.options.shutdownTimeout}ms`
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
return Result.ok(undefined);
|
|
121
|
+
}
|
|
122
|
+
const daemon = {
|
|
123
|
+
get state() {
|
|
124
|
+
return internalState.state;
|
|
125
|
+
},
|
|
126
|
+
async start() {
|
|
127
|
+
const { logger } = internalState.options;
|
|
128
|
+
if (internalState.state !== "stopped") {
|
|
129
|
+
return Result.err(new DaemonError({
|
|
130
|
+
code: "ALREADY_RUNNING",
|
|
131
|
+
message: `Daemon "${internalState.options.name}" is already running`
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
internalState.state = "starting";
|
|
135
|
+
logger?.info("Daemon starting", { name: internalState.options.name });
|
|
136
|
+
if (await pidFileExists(internalState.options.pidFile)) {
|
|
137
|
+
internalState.state = "stopped";
|
|
138
|
+
return Result.err(new DaemonError({
|
|
139
|
+
code: "ALREADY_RUNNING",
|
|
140
|
+
message: `PID file already exists: ${internalState.options.pidFile}`
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
const writeResult = await writePidFile(internalState.options.pidFile, process.pid);
|
|
144
|
+
if (writeResult.isErr()) {
|
|
145
|
+
internalState.state = "stopped";
|
|
146
|
+
return writeResult;
|
|
147
|
+
}
|
|
148
|
+
const sigTermHandler = () => {
|
|
149
|
+
logger?.info("Received SIGTERM signal");
|
|
150
|
+
doStop();
|
|
151
|
+
};
|
|
152
|
+
const sigIntHandler = () => {
|
|
153
|
+
logger?.info("Received SIGINT signal");
|
|
154
|
+
doStop();
|
|
155
|
+
};
|
|
156
|
+
internalState.signalHandlers.sigterm = sigTermHandler;
|
|
157
|
+
internalState.signalHandlers.sigint = sigIntHandler;
|
|
158
|
+
process.on("SIGTERM", sigTermHandler);
|
|
159
|
+
process.on("SIGINT", sigIntHandler);
|
|
160
|
+
internalState.state = "running";
|
|
161
|
+
logger?.info("Daemon started", {
|
|
162
|
+
name: internalState.options.name,
|
|
163
|
+
pid: process.pid
|
|
164
|
+
});
|
|
165
|
+
return Result.ok(undefined);
|
|
166
|
+
},
|
|
167
|
+
stop() {
|
|
168
|
+
return doStop();
|
|
169
|
+
},
|
|
170
|
+
isRunning() {
|
|
171
|
+
return internalState.state === "running";
|
|
172
|
+
},
|
|
173
|
+
onShutdown(handler) {
|
|
174
|
+
internalState.shutdownHandlers.push(handler);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return daemon;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { createDaemon };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Result } from "@outfitter/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* A single health check definition.
|
|
4
|
+
*
|
|
5
|
+
* Health checks are used to verify that a service or resource is functioning
|
|
6
|
+
* correctly. Each check has a name for identification and a check function
|
|
7
|
+
* that returns a Result indicating success or failure.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const databaseCheck: HealthCheck = {
|
|
12
|
+
* name: "database",
|
|
13
|
+
* check: async () => {
|
|
14
|
+
* try {
|
|
15
|
+
* await db.ping();
|
|
16
|
+
* return Result.ok(undefined);
|
|
17
|
+
* } catch (error) {
|
|
18
|
+
* return Result.err(error);
|
|
19
|
+
* }
|
|
20
|
+
* },
|
|
21
|
+
* };
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
interface HealthCheck {
|
|
25
|
+
/**
|
|
26
|
+
* Unique name identifying this health check.
|
|
27
|
+
* Used as the key in the HealthStatus.checks record.
|
|
28
|
+
*/
|
|
29
|
+
name: string;
|
|
30
|
+
/**
|
|
31
|
+
* Function that performs the health check.
|
|
32
|
+
*
|
|
33
|
+
* Should return Result.ok(undefined) if healthy, or Result.err(error)
|
|
34
|
+
* with details about the failure.
|
|
35
|
+
*/
|
|
36
|
+
check(): Promise<Result<void, Error>>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Result of an individual health check.
|
|
40
|
+
*
|
|
41
|
+
* Contains the healthy/unhealthy status and an optional message
|
|
42
|
+
* providing more details (typically the error message on failure).
|
|
43
|
+
*/
|
|
44
|
+
interface HealthCheckResult {
|
|
45
|
+
/** Whether this check passed (true) or failed (false) */
|
|
46
|
+
healthy: boolean;
|
|
47
|
+
/** Optional message, typically the error message on failure */
|
|
48
|
+
message?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Aggregated health status from all registered checks.
|
|
52
|
+
*
|
|
53
|
+
* The overall healthy status is true only if ALL individual checks pass.
|
|
54
|
+
* Includes the uptime of the health checker in seconds.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const status: HealthStatus = {
|
|
59
|
+
* healthy: false,
|
|
60
|
+
* checks: {
|
|
61
|
+
* database: { healthy: true },
|
|
62
|
+
* cache: { healthy: false, message: "Connection refused" },
|
|
63
|
+
* },
|
|
64
|
+
* uptime: 3600,
|
|
65
|
+
* };
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
interface HealthStatus {
|
|
69
|
+
/** Overall health status - true only if ALL checks pass */
|
|
70
|
+
healthy: boolean;
|
|
71
|
+
/** Individual check results keyed by check name */
|
|
72
|
+
checks: Record<string, HealthCheckResult>;
|
|
73
|
+
/** Uptime in seconds since the health checker was created */
|
|
74
|
+
uptime: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Health checker interface for managing and running health checks.
|
|
78
|
+
*
|
|
79
|
+
* Provides methods to run all registered checks and get the aggregated
|
|
80
|
+
* health status, as well as dynamically registering new checks at runtime.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const checker = createHealthChecker([
|
|
85
|
+
* { name: "db", check: checkDatabase },
|
|
86
|
+
* { name: "cache", check: checkCache },
|
|
87
|
+
* ]);
|
|
88
|
+
*
|
|
89
|
+
* // Later, add more checks
|
|
90
|
+
* checker.register({ name: "queue", check: checkQueue });
|
|
91
|
+
*
|
|
92
|
+
* // Get health status
|
|
93
|
+
* const status = await checker.check();
|
|
94
|
+
* console.log("Healthy:", status.healthy);
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
interface HealthChecker {
|
|
98
|
+
/**
|
|
99
|
+
* Run all registered health checks and return aggregated status.
|
|
100
|
+
*
|
|
101
|
+
* Checks are run in parallel for efficiency. The overall healthy
|
|
102
|
+
* status is true only if all individual checks pass.
|
|
103
|
+
*
|
|
104
|
+
* @returns Aggregated health status
|
|
105
|
+
*/
|
|
106
|
+
check(): Promise<HealthStatus>;
|
|
107
|
+
/**
|
|
108
|
+
* Register a new health check at runtime.
|
|
109
|
+
*
|
|
110
|
+
* The check will be included in all subsequent calls to check().
|
|
111
|
+
*
|
|
112
|
+
* @param check - Health check to register
|
|
113
|
+
*/
|
|
114
|
+
register(check: HealthCheck): void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create a health checker with initial checks.
|
|
118
|
+
*
|
|
119
|
+
* The health checker runs all registered checks in parallel and aggregates
|
|
120
|
+
* their results. Individual check failures are isolated and don't prevent
|
|
121
|
+
* other checks from running.
|
|
122
|
+
*
|
|
123
|
+
* @param checks - Initial health checks to register
|
|
124
|
+
* @returns HealthChecker instance
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const checker = createHealthChecker([
|
|
129
|
+
* {
|
|
130
|
+
* name: "database",
|
|
131
|
+
* check: async () => {
|
|
132
|
+
* await db.ping();
|
|
133
|
+
* return Result.ok(undefined);
|
|
134
|
+
* },
|
|
135
|
+
* },
|
|
136
|
+
* {
|
|
137
|
+
* name: "cache",
|
|
138
|
+
* check: async () => {
|
|
139
|
+
* await redis.ping();
|
|
140
|
+
* return Result.ok(undefined);
|
|
141
|
+
* },
|
|
142
|
+
* },
|
|
143
|
+
* ]);
|
|
144
|
+
*
|
|
145
|
+
* // Run health checks
|
|
146
|
+
* const status = await checker.check();
|
|
147
|
+
*
|
|
148
|
+
* if (!status.healthy) {
|
|
149
|
+
* console.error("Service unhealthy:", status.checks);
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
declare function createHealthChecker(checks: HealthCheck[]): HealthChecker;
|
|
154
|
+
export { HealthCheck, HealthCheckResult, HealthStatus, HealthChecker, createHealthChecker };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if running on a Unix-like platform (macOS or Linux).
|
|
3
|
+
*
|
|
4
|
+
* @returns true on macOS/Linux, false on Windows
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* if (isUnixPlatform()) {
|
|
9
|
+
* // Use Unix domain sockets
|
|
10
|
+
* } else {
|
|
11
|
+
* // Use named pipes
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
declare function isUnixPlatform(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Get the Unix domain socket path for a tool's daemon.
|
|
18
|
+
*
|
|
19
|
+
* @param toolName - Name of the tool (e.g., "waymark", "firewatch")
|
|
20
|
+
* @returns Absolute path to the daemon socket
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const socketPath = getSocketPath("waymark");
|
|
25
|
+
* // "/run/user/1000/waymark/daemon.sock" on Linux
|
|
26
|
+
* // "/var/folders/.../waymark/daemon.sock" on macOS
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function getSocketPath(toolName: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get the lock file path for a tool's daemon.
|
|
32
|
+
*
|
|
33
|
+
* @param toolName - Name of the tool (e.g., "waymark", "firewatch")
|
|
34
|
+
* @returns Absolute path to the daemon lock file
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const lockPath = getLockPath("waymark");
|
|
39
|
+
* // "/run/user/1000/waymark/daemon.lock" on Linux
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function getLockPath(toolName: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Get the PID file path for a tool's daemon.
|
|
45
|
+
*
|
|
46
|
+
* @param toolName - Name of the tool (e.g., "waymark", "firewatch")
|
|
47
|
+
* @returns Absolute path to the daemon PID file
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const pidPath = getPidPath("waymark");
|
|
52
|
+
* // "/run/user/1000/waymark/daemon.pid" on Linux
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function getPidPath(toolName: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Get the directory containing daemon files for a tool.
|
|
58
|
+
*
|
|
59
|
+
* @param toolName - Name of the tool (e.g., "waymark", "firewatch")
|
|
60
|
+
* @returns Absolute path to the daemon directory
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const daemonDir = getDaemonDir("waymark");
|
|
65
|
+
* // "/run/user/1000/waymark" on Linux
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function getDaemonDir(toolName: string): string;
|
|
69
|
+
export { isUnixPlatform, getSocketPath, getLockPath, getPidPath, getDaemonDir };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message handler type for processing incoming IPC messages.
|
|
3
|
+
*
|
|
4
|
+
* Receives a parsed message and returns a response to send back to the client.
|
|
5
|
+
* Throwing an error will result in an error response to the client.
|
|
6
|
+
*/
|
|
7
|
+
type IpcMessageHandler = (message: unknown) => Promise<unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* IPC server interface for receiving messages from clients.
|
|
10
|
+
*
|
|
11
|
+
* The server listens on a Unix socket and processes incoming messages
|
|
12
|
+
* using the registered message handler.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const server = createIpcServer("/var/run/my-daemon.sock");
|
|
17
|
+
*
|
|
18
|
+
* server.onMessage(async (msg) => {
|
|
19
|
+
* if (msg.type === "status") {
|
|
20
|
+
* return { status: "ok", uptime: process.uptime() };
|
|
21
|
+
* }
|
|
22
|
+
* return { error: "Unknown command" };
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await server.listen();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
interface IpcServer {
|
|
29
|
+
/**
|
|
30
|
+
* Start listening for connections on the Unix socket.
|
|
31
|
+
*
|
|
32
|
+
* Creates the socket file and begins accepting client connections.
|
|
33
|
+
* Messages are processed using the handler registered via onMessage.
|
|
34
|
+
*/
|
|
35
|
+
listen(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Stop listening and close all connections.
|
|
38
|
+
*
|
|
39
|
+
* Removes the socket file and cleans up resources.
|
|
40
|
+
*/
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Register a message handler for incoming messages.
|
|
44
|
+
*
|
|
45
|
+
* Only one handler can be registered. Calling this multiple times
|
|
46
|
+
* replaces the previous handler.
|
|
47
|
+
*
|
|
48
|
+
* @param handler - Function to process incoming messages
|
|
49
|
+
*/
|
|
50
|
+
onMessage(handler: IpcMessageHandler): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* IPC client interface for sending messages to a server.
|
|
54
|
+
*
|
|
55
|
+
* The client connects to a Unix socket and can send messages,
|
|
56
|
+
* receiving responses asynchronously.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const client = createIpcClient("/var/run/my-daemon.sock");
|
|
61
|
+
*
|
|
62
|
+
* await client.connect();
|
|
63
|
+
*
|
|
64
|
+
* const response = await client.send<StatusResponse>({ type: "status" });
|
|
65
|
+
* console.log("Daemon uptime:", response.uptime);
|
|
66
|
+
*
|
|
67
|
+
* client.close();
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
interface IpcClient {
|
|
71
|
+
/**
|
|
72
|
+
* Connect to the IPC server.
|
|
73
|
+
*
|
|
74
|
+
* Establishes a connection to the Unix socket. Throws if the
|
|
75
|
+
* server is not available.
|
|
76
|
+
*/
|
|
77
|
+
connect(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Send a message and wait for a response.
|
|
80
|
+
*
|
|
81
|
+
* Serializes the message to JSON, sends it to the server, and
|
|
82
|
+
* waits for a response.
|
|
83
|
+
*
|
|
84
|
+
* @typeParam T - Expected response type
|
|
85
|
+
* @param message - Message to send (must be JSON-serializable)
|
|
86
|
+
* @returns Promise resolving to the server's response
|
|
87
|
+
* @throws Error if not connected or communication fails
|
|
88
|
+
*/
|
|
89
|
+
send<T>(message: unknown): Promise<T>;
|
|
90
|
+
/**
|
|
91
|
+
* Close the connection to the server.
|
|
92
|
+
*
|
|
93
|
+
* Can be called multiple times safely.
|
|
94
|
+
*/
|
|
95
|
+
close(): void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create an IPC server listening on a Unix socket.
|
|
99
|
+
*
|
|
100
|
+
* @param socketPath - Path to the Unix socket file
|
|
101
|
+
* @returns IpcServer instance
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const server = createIpcServer("/var/run/my-daemon.sock");
|
|
106
|
+
*
|
|
107
|
+
* server.onMessage(async (msg) => {
|
|
108
|
+
* return { echo: msg };
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* await server.listen();
|
|
112
|
+
* // Server is now accepting connections
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
declare function createIpcServer(socketPath: string): IpcServer;
|
|
116
|
+
/**
|
|
117
|
+
* Create an IPC client for connecting to a server.
|
|
118
|
+
*
|
|
119
|
+
* @param socketPath - Path to the Unix socket file
|
|
120
|
+
* @returns IpcClient instance
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* const client = createIpcClient("/var/run/my-daemon.sock");
|
|
125
|
+
*
|
|
126
|
+
* await client.connect();
|
|
127
|
+
* const response = await client.send({ command: "status" });
|
|
128
|
+
* console.log(response);
|
|
129
|
+
* client.close();
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
declare function createIpcClient(socketPath: string): IpcClient;
|
|
133
|
+
export { IpcMessageHandler, IpcServer, IpcClient, createIpcServer, createIpcClient };
|