@outfitter/daemon 0.2.1 → 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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { TaggedErrorClass } from "@outfitter/contracts";
|
|
2
|
+
declare const StaleSocketErrorBase: TaggedErrorClass<"StaleSocketError", {
|
|
3
|
+
message: string;
|
|
4
|
+
socketPath: string;
|
|
5
|
+
pid?: number;
|
|
6
|
+
}>;
|
|
7
|
+
declare const ConnectionRefusedErrorBase: TaggedErrorClass<"ConnectionRefusedError", {
|
|
8
|
+
message: string;
|
|
9
|
+
socketPath: string;
|
|
10
|
+
}>;
|
|
11
|
+
declare const ConnectionTimeoutErrorBase: TaggedErrorClass<"ConnectionTimeoutError", {
|
|
12
|
+
message: string;
|
|
13
|
+
socketPath: string;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
}>;
|
|
16
|
+
declare const ProtocolErrorBase: TaggedErrorClass<"ProtocolError", {
|
|
17
|
+
message: string;
|
|
18
|
+
socketPath: string;
|
|
19
|
+
details?: string;
|
|
20
|
+
}>;
|
|
21
|
+
declare const LockErrorBase: TaggedErrorClass<"LockError", {
|
|
22
|
+
message: string;
|
|
23
|
+
lockPath: string;
|
|
24
|
+
pid?: number;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Socket exists but daemon process is dead.
|
|
28
|
+
*
|
|
29
|
+
* Indicates a stale socket from a crashed daemon that needs cleanup
|
|
30
|
+
* before a new daemon can start.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const error = new StaleSocketError({
|
|
35
|
+
* message: "Daemon socket is stale",
|
|
36
|
+
* socketPath: "/run/user/1000/waymark/daemon.sock",
|
|
37
|
+
* pid: 12345,
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare class StaleSocketError extends StaleSocketErrorBase {}
|
|
42
|
+
/**
|
|
43
|
+
* Daemon is not running (connection refused).
|
|
44
|
+
*
|
|
45
|
+
* Socket does not exist or connection was actively refused,
|
|
46
|
+
* indicating no daemon is listening.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const error = new ConnectionRefusedError({
|
|
51
|
+
* message: "Connection refused",
|
|
52
|
+
* socketPath: "/run/user/1000/waymark/daemon.sock",
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
declare class ConnectionRefusedError extends ConnectionRefusedErrorBase {}
|
|
57
|
+
/**
|
|
58
|
+
* Daemon did not respond within timeout.
|
|
59
|
+
*
|
|
60
|
+
* Connection was established but daemon failed to respond
|
|
61
|
+
* to ping or request within the configured timeout.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const error = new ConnectionTimeoutError({
|
|
66
|
+
* message: "Connection timed out after 5000ms",
|
|
67
|
+
* socketPath: "/run/user/1000/waymark/daemon.sock",
|
|
68
|
+
* timeoutMs: 5000,
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare class ConnectionTimeoutError extends ConnectionTimeoutErrorBase {}
|
|
73
|
+
/**
|
|
74
|
+
* Invalid response format from daemon.
|
|
75
|
+
*
|
|
76
|
+
* Daemon responded but the response could not be parsed
|
|
77
|
+
* or did not match the expected protocol format.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const error = new ProtocolError({
|
|
82
|
+
* message: "Invalid JSON response",
|
|
83
|
+
* socketPath: "/run/user/1000/waymark/daemon.sock",
|
|
84
|
+
* details: "Unexpected token at position 42",
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare class ProtocolError extends ProtocolErrorBase {}
|
|
89
|
+
/**
|
|
90
|
+
* Failed to acquire or release daemon lock.
|
|
91
|
+
*
|
|
92
|
+
* Used when PID file operations fail due to permissions,
|
|
93
|
+
* concurrent access, or file system errors.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const error = new LockError({
|
|
98
|
+
* message: "Daemon already running",
|
|
99
|
+
* lockPath: "/run/user/1000/waymark/daemon.lock",
|
|
100
|
+
* pid: 12345,
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare class LockError extends LockErrorBase {}
|
|
105
|
+
/**
|
|
106
|
+
* Union of all daemon connection error types.
|
|
107
|
+
*
|
|
108
|
+
* Use for exhaustive matching on connection failures:
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* function handleError(error: DaemonConnectionError): string {
|
|
113
|
+
* switch (error._tag) {
|
|
114
|
+
* case "StaleSocketError":
|
|
115
|
+
* return `Stale socket at ${error.socketPath}`;
|
|
116
|
+
* case "ConnectionRefusedError":
|
|
117
|
+
* return "Daemon not running";
|
|
118
|
+
* case "ConnectionTimeoutError":
|
|
119
|
+
* return `Timeout after ${error.timeoutMs}ms`;
|
|
120
|
+
* case "ProtocolError":
|
|
121
|
+
* return `Protocol error: ${error.details}`;
|
|
122
|
+
* }
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
type DaemonConnectionError = StaleSocketError | ConnectionRefusedError | ConnectionTimeoutError | ProtocolError;
|
|
127
|
+
export { StaleSocketError, ConnectionRefusedError, ConnectionTimeoutError, ProtocolError, LockError, DaemonConnectionError };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
LockError
|
|
4
|
+
} from "./daemon-qqn2jpsg.js";
|
|
5
|
+
|
|
6
|
+
// packages/daemon/src/locking.ts
|
|
7
|
+
import { unlink } from "fs/promises";
|
|
8
|
+
function isProcessAlive(pid) {
|
|
9
|
+
if (pid <= 0) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof Error && "code" in error && error.code === "EPERM") {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function isDaemonAlive(lockPath) {
|
|
23
|
+
const file = Bun.file(lockPath);
|
|
24
|
+
if (!await file.exists()) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const content = await file.text();
|
|
29
|
+
const pid = Number.parseInt(content.trim(), 10);
|
|
30
|
+
if (Number.isNaN(pid)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return isProcessAlive(pid);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function acquireDaemonLock(lockPath) {
|
|
39
|
+
const file = Bun.file(lockPath);
|
|
40
|
+
if (await file.exists()) {
|
|
41
|
+
try {
|
|
42
|
+
const content = await file.text();
|
|
43
|
+
const existingPid = Number.parseInt(content.trim(), 10);
|
|
44
|
+
if (!Number.isNaN(existingPid) && isProcessAlive(existingPid)) {
|
|
45
|
+
return {
|
|
46
|
+
isOk: () => false,
|
|
47
|
+
isErr: () => true,
|
|
48
|
+
error: new LockError({
|
|
49
|
+
message: `Daemon already running with PID ${existingPid}`,
|
|
50
|
+
lockPath,
|
|
51
|
+
pid: existingPid
|
|
52
|
+
})
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
const pid = process.pid;
|
|
58
|
+
try {
|
|
59
|
+
await Bun.write(lockPath, `${pid}
|
|
60
|
+
`);
|
|
61
|
+
return {
|
|
62
|
+
isOk: () => true,
|
|
63
|
+
isErr: () => false,
|
|
64
|
+
value: { lockPath, pid }
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
isOk: () => false,
|
|
69
|
+
isErr: () => true,
|
|
70
|
+
error: new LockError({
|
|
71
|
+
message: `Failed to write lock file: ${error instanceof Error ? error.message : String(error)}`,
|
|
72
|
+
lockPath,
|
|
73
|
+
pid
|
|
74
|
+
})
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function releaseDaemonLock(handle) {
|
|
79
|
+
const { lockPath, pid } = handle;
|
|
80
|
+
const file = Bun.file(lockPath);
|
|
81
|
+
if (!await file.exists()) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const content = await file.text();
|
|
86
|
+
const filePid = Number.parseInt(content.trim(), 10);
|
|
87
|
+
if (filePid === pid) {
|
|
88
|
+
await unlink(lockPath);
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
async function readLockPid(lockPath) {
|
|
93
|
+
const file = Bun.file(lockPath);
|
|
94
|
+
if (!await file.exists()) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const content = await file.text();
|
|
99
|
+
const pid = Number.parseInt(content.trim(), 10);
|
|
100
|
+
return Number.isNaN(pid) ? undefined : pid;
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { isProcessAlive, isDaemonAlive, acquireDaemonLock, releaseDaemonLock, readLockPid };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Result, TaggedErrorClass } from "@outfitter/contracts";
|
|
2
|
+
import { LoggerInstance } from "@outfitter/logging";
|
|
3
|
+
declare const DaemonErrorBase: TaggedErrorClass<"DaemonError", {
|
|
4
|
+
code: DaemonErrorCode;
|
|
5
|
+
message: string;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Error codes for daemon operations.
|
|
9
|
+
*
|
|
10
|
+
* - `ALREADY_RUNNING`: Daemon start requested but already running
|
|
11
|
+
* - `NOT_RUNNING`: Daemon stop requested but not running
|
|
12
|
+
* - `SHUTDOWN_TIMEOUT`: Graceful shutdown exceeded timeout
|
|
13
|
+
* - `PID_ERROR`: PID file operations failed
|
|
14
|
+
* - `START_FAILED`: Daemon failed to start
|
|
15
|
+
*/
|
|
16
|
+
type DaemonErrorCode = "ALREADY_RUNNING" | "NOT_RUNNING" | "SHUTDOWN_TIMEOUT" | "PID_ERROR" | "START_FAILED";
|
|
17
|
+
/**
|
|
18
|
+
* Error type for daemon operations.
|
|
19
|
+
*
|
|
20
|
+
* Uses the TaggedError pattern for type-safe error handling with Result types.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const error = new DaemonError({
|
|
25
|
+
* code: "ALREADY_RUNNING",
|
|
26
|
+
* message: "Daemon is already running with PID 1234",
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* if (error.code === "ALREADY_RUNNING") {
|
|
30
|
+
* console.log("Stop the existing daemon first");
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare class DaemonError extends DaemonErrorBase {}
|
|
35
|
+
/**
|
|
36
|
+
* Daemon lifecycle states.
|
|
37
|
+
*
|
|
38
|
+
* State machine transitions:
|
|
39
|
+
* - `stopped` -> `starting` (via start())
|
|
40
|
+
* - `starting` -> `running` (when initialization complete)
|
|
41
|
+
* - `starting` -> `stopped` (if start fails)
|
|
42
|
+
* - `running` -> `stopping` (via stop() or signal)
|
|
43
|
+
* - `stopping` -> `stopped` (when shutdown complete)
|
|
44
|
+
*/
|
|
45
|
+
type DaemonState = "stopped" | "starting" | "running" | "stopping";
|
|
46
|
+
/**
|
|
47
|
+
* Configuration options for creating a daemon.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const options: DaemonOptions = {
|
|
52
|
+
* name: "my-daemon",
|
|
53
|
+
* pidFile: "/var/run/my-daemon.pid",
|
|
54
|
+
* logger: myLogger,
|
|
55
|
+
* shutdownTimeout: 10000, // 10 seconds
|
|
56
|
+
* };
|
|
57
|
+
*
|
|
58
|
+
* const daemon = createDaemon(options);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
interface DaemonOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Unique name identifying this daemon.
|
|
64
|
+
* Used in log messages and error context.
|
|
65
|
+
*/
|
|
66
|
+
name: string;
|
|
67
|
+
/**
|
|
68
|
+
* Absolute path to the PID file.
|
|
69
|
+
* The daemon writes its process ID here on start and removes it on stop.
|
|
70
|
+
* Used to prevent multiple instances and for external process management.
|
|
71
|
+
*/
|
|
72
|
+
pidFile: string;
|
|
73
|
+
/**
|
|
74
|
+
* Optional logger instance for daemon messages.
|
|
75
|
+
* If not provided, logging is disabled.
|
|
76
|
+
*/
|
|
77
|
+
logger?: LoggerInstance;
|
|
78
|
+
/**
|
|
79
|
+
* Maximum time in milliseconds to wait for graceful shutdown.
|
|
80
|
+
* After this timeout, the daemon will force stop.
|
|
81
|
+
* @defaultValue 5000
|
|
82
|
+
*/
|
|
83
|
+
shutdownTimeout?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Shutdown handler function type.
|
|
87
|
+
*
|
|
88
|
+
* Called during graceful shutdown to allow cleanup of resources.
|
|
89
|
+
* Must complete within the shutdown timeout.
|
|
90
|
+
*/
|
|
91
|
+
type ShutdownHandler = () => Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Daemon instance interface.
|
|
94
|
+
*
|
|
95
|
+
* Provides lifecycle management for a background process including
|
|
96
|
+
* start/stop operations, signal handling, and shutdown hooks.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const daemon = createDaemon({
|
|
101
|
+
* name: "my-service",
|
|
102
|
+
* pidFile: "/var/run/my-service.pid",
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Register cleanup handlers
|
|
106
|
+
* daemon.onShutdown(async () => {
|
|
107
|
+
* await database.close();
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* // Start the daemon
|
|
111
|
+
* const result = await daemon.start();
|
|
112
|
+
* if (result.isErr()) {
|
|
113
|
+
* console.error("Failed to start:", result.error.message);
|
|
114
|
+
* process.exit(1);
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* // Daemon is now running...
|
|
118
|
+
* // Stop gracefully when needed
|
|
119
|
+
* await daemon.stop();
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
interface Daemon {
|
|
123
|
+
/**
|
|
124
|
+
* Current lifecycle state of the daemon.
|
|
125
|
+
*/
|
|
126
|
+
readonly state: DaemonState;
|
|
127
|
+
/**
|
|
128
|
+
* Start the daemon.
|
|
129
|
+
*
|
|
130
|
+
* Creates PID file and registers signal handlers.
|
|
131
|
+
* Transitions from `stopped` to `starting` then `running`.
|
|
132
|
+
*
|
|
133
|
+
* @returns Result with void on success, or DaemonError on failure
|
|
134
|
+
*/
|
|
135
|
+
start(): Promise<Result<void, DaemonError>>;
|
|
136
|
+
/**
|
|
137
|
+
* Stop the daemon gracefully.
|
|
138
|
+
*
|
|
139
|
+
* Runs shutdown handlers, removes PID file, and cleans up signal handlers.
|
|
140
|
+
* Transitions from `running` to `stopping` then `stopped`.
|
|
141
|
+
*
|
|
142
|
+
* @returns Result with void on success, or DaemonError on failure
|
|
143
|
+
*/
|
|
144
|
+
stop(): Promise<Result<void, DaemonError>>;
|
|
145
|
+
/**
|
|
146
|
+
* Check if the daemon is currently running.
|
|
147
|
+
*
|
|
148
|
+
* @returns true if state is "running", false otherwise
|
|
149
|
+
*/
|
|
150
|
+
isRunning(): boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Register a shutdown handler to be called during graceful shutdown.
|
|
153
|
+
*
|
|
154
|
+
* Multiple handlers can be registered and will be called in registration order.
|
|
155
|
+
* Handlers must complete within the shutdown timeout.
|
|
156
|
+
*
|
|
157
|
+
* @param handler - Async function to execute during shutdown
|
|
158
|
+
*/
|
|
159
|
+
onShutdown(handler: ShutdownHandler): void;
|
|
160
|
+
}
|
|
161
|
+
export { DaemonErrorCode, DaemonError, DaemonState, DaemonOptions, ShutdownHandler, Daemon };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Daemon, DaemonOptions } from "./daemon-9w2ey87r";
|
|
2
|
+
/**
|
|
3
|
+
* Create a new daemon instance.
|
|
4
|
+
*
|
|
5
|
+
* The daemon manages its own lifecycle including PID file creation/removal,
|
|
6
|
+
* signal handling for graceful shutdown, and execution of registered
|
|
7
|
+
* shutdown handlers.
|
|
8
|
+
*
|
|
9
|
+
* @param options - Daemon configuration options
|
|
10
|
+
* @returns Daemon instance
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const daemon = createDaemon({
|
|
15
|
+
* name: "my-service",
|
|
16
|
+
* pidFile: "/var/run/my-service.pid",
|
|
17
|
+
* shutdownTimeout: 10000,
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* daemon.onShutdown(async () => {
|
|
21
|
+
* await database.close();
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const result = await daemon.start();
|
|
25
|
+
* if (result.isErr()) {
|
|
26
|
+
* console.error("Failed to start:", result.error.message);
|
|
27
|
+
* process.exit(1);
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function createDaemon(options: DaemonOptions): Daemon;
|
|
32
|
+
export { createDaemon };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/daemon/src/health.ts
|
|
3
|
+
function createHealthChecker(checks) {
|
|
4
|
+
const registeredChecks = [...checks];
|
|
5
|
+
const startTime = Date.now();
|
|
6
|
+
async function runCheck(check) {
|
|
7
|
+
try {
|
|
8
|
+
const result = await check.check();
|
|
9
|
+
if (result.isOk()) {
|
|
10
|
+
return { healthy: true };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
healthy: false,
|
|
14
|
+
message: result.error.message
|
|
15
|
+
};
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return {
|
|
18
|
+
healthy: false,
|
|
19
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
async check() {
|
|
25
|
+
const results = await Promise.all(registeredChecks.map(async (check) => ({
|
|
26
|
+
name: check.name,
|
|
27
|
+
result: await runCheck(check)
|
|
28
|
+
})));
|
|
29
|
+
const checksRecord = {};
|
|
30
|
+
let allHealthy = true;
|
|
31
|
+
for (const { name, result } of results) {
|
|
32
|
+
checksRecord[name] = result;
|
|
33
|
+
if (!result.healthy) {
|
|
34
|
+
allHealthy = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
|
38
|
+
return {
|
|
39
|
+
healthy: allHealthy,
|
|
40
|
+
checks: checksRecord,
|
|
41
|
+
uptime
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
register(check) {
|
|
45
|
+
registeredChecks.push(check);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { createHealthChecker };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/daemon/src/ipc.ts
|
|
3
|
+
import { unlink } from "fs/promises";
|
|
4
|
+
function createIpcServer(socketPath) {
|
|
5
|
+
let messageHandler = null;
|
|
6
|
+
let server = null;
|
|
7
|
+
let isListening = false;
|
|
8
|
+
const socketBuffers = new WeakMap;
|
|
9
|
+
function processSocketBuffer(socket) {
|
|
10
|
+
const buffer = socketBuffers.get(socket) ?? "";
|
|
11
|
+
const lines = buffer.split(`
|
|
12
|
+
`);
|
|
13
|
+
socketBuffers.set(socket, lines.pop() ?? "");
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (!line.trim())
|
|
16
|
+
continue;
|
|
17
|
+
try {
|
|
18
|
+
const message = JSON.parse(line);
|
|
19
|
+
if (message.type === "request" && messageHandler) {
|
|
20
|
+
(async () => {
|
|
21
|
+
try {
|
|
22
|
+
const result = await messageHandler(message.payload);
|
|
23
|
+
const response = {
|
|
24
|
+
id: message.id,
|
|
25
|
+
type: "response",
|
|
26
|
+
payload: result
|
|
27
|
+
};
|
|
28
|
+
socket.write(`${JSON.stringify(response)}
|
|
29
|
+
`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const errorResponse = {
|
|
32
|
+
id: message.id,
|
|
33
|
+
type: "error",
|
|
34
|
+
payload: {
|
|
35
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
socket.write(`${JSON.stringify(errorResponse)}
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
async listen() {
|
|
48
|
+
if (isListening)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
await unlink(socketPath);
|
|
52
|
+
} catch {}
|
|
53
|
+
server = Bun.listen({
|
|
54
|
+
unix: socketPath,
|
|
55
|
+
socket: {
|
|
56
|
+
data(socket, data) {
|
|
57
|
+
const text = Buffer.isBuffer(data) ? data.toString("utf-8") : String(data);
|
|
58
|
+
const currentBuffer = socketBuffers.get(socket) ?? "";
|
|
59
|
+
socketBuffers.set(socket, currentBuffer + text);
|
|
60
|
+
processSocketBuffer(socket);
|
|
61
|
+
},
|
|
62
|
+
open(socket) {
|
|
63
|
+
socketBuffers.set(socket, "");
|
|
64
|
+
},
|
|
65
|
+
close(socket) {
|
|
66
|
+
socketBuffers.delete(socket);
|
|
67
|
+
},
|
|
68
|
+
error() {}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
isListening = true;
|
|
72
|
+
},
|
|
73
|
+
async close() {
|
|
74
|
+
if (!isListening)
|
|
75
|
+
return;
|
|
76
|
+
server?.stop();
|
|
77
|
+
server = null;
|
|
78
|
+
isListening = false;
|
|
79
|
+
try {
|
|
80
|
+
await unlink(socketPath);
|
|
81
|
+
} catch {}
|
|
82
|
+
},
|
|
83
|
+
onMessage(handler) {
|
|
84
|
+
messageHandler = handler;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function createIpcClient(socketPath) {
|
|
89
|
+
let socket = null;
|
|
90
|
+
let isConnected = false;
|
|
91
|
+
const pendingRequests = new Map;
|
|
92
|
+
let messageBuffer = "";
|
|
93
|
+
function generateId() {
|
|
94
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
95
|
+
}
|
|
96
|
+
function processBuffer() {
|
|
97
|
+
const lines = messageBuffer.split(`
|
|
98
|
+
`);
|
|
99
|
+
messageBuffer = lines.pop() ?? "";
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (!line.trim())
|
|
102
|
+
continue;
|
|
103
|
+
try {
|
|
104
|
+
const message = JSON.parse(line);
|
|
105
|
+
const pending = pendingRequests.get(message.id);
|
|
106
|
+
if (pending) {
|
|
107
|
+
pendingRequests.delete(message.id);
|
|
108
|
+
if (message.type === "response") {
|
|
109
|
+
pending.resolve(message.payload);
|
|
110
|
+
} else if (message.type === "error") {
|
|
111
|
+
const errorPayload = message.payload;
|
|
112
|
+
pending.reject(new Error(errorPayload.message));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function rejectAllPending() {
|
|
119
|
+
for (const [id, pending] of pendingRequests) {
|
|
120
|
+
pending.reject(new Error("Connection closed"));
|
|
121
|
+
pendingRequests.delete(id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
connect() {
|
|
126
|
+
if (isConnected && socket)
|
|
127
|
+
return Promise.resolve();
|
|
128
|
+
messageBuffer = "";
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
try {
|
|
131
|
+
Bun.connect({
|
|
132
|
+
unix: socketPath,
|
|
133
|
+
socket: {
|
|
134
|
+
data(_socket, data) {
|
|
135
|
+
const text = Buffer.isBuffer(data) ? data.toString("utf-8") : String(data);
|
|
136
|
+
messageBuffer += text;
|
|
137
|
+
processBuffer();
|
|
138
|
+
},
|
|
139
|
+
open(_socket) {
|
|
140
|
+
isConnected = true;
|
|
141
|
+
socket = _socket;
|
|
142
|
+
resolve();
|
|
143
|
+
},
|
|
144
|
+
close() {
|
|
145
|
+
isConnected = false;
|
|
146
|
+
socket = null;
|
|
147
|
+
rejectAllPending();
|
|
148
|
+
},
|
|
149
|
+
error(_socket, error) {
|
|
150
|
+
isConnected = false;
|
|
151
|
+
socket = null;
|
|
152
|
+
reject(error);
|
|
153
|
+
},
|
|
154
|
+
connectError(_socket, error) {
|
|
155
|
+
isConnected = false;
|
|
156
|
+
socket = null;
|
|
157
|
+
reject(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
reject(error);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
send(message) {
|
|
167
|
+
if (!(isConnected && socket)) {
|
|
168
|
+
return Promise.reject(new Error("Not connected to server"));
|
|
169
|
+
}
|
|
170
|
+
const id = generateId();
|
|
171
|
+
const request = {
|
|
172
|
+
id,
|
|
173
|
+
type: "request",
|
|
174
|
+
payload: message
|
|
175
|
+
};
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
pendingRequests.set(id, {
|
|
178
|
+
resolve,
|
|
179
|
+
reject
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
const written = socket?.write(`${JSON.stringify(request)}
|
|
183
|
+
`);
|
|
184
|
+
if (written === 0) {
|
|
185
|
+
pendingRequests.delete(id);
|
|
186
|
+
reject(new Error("Failed to write to socket"));
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
pendingRequests.delete(id);
|
|
190
|
+
reject(error);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
close() {
|
|
195
|
+
if (!(isConnected && socket))
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
socket?.terminate();
|
|
199
|
+
} catch {}
|
|
200
|
+
socket = null;
|
|
201
|
+
isConnected = false;
|
|
202
|
+
rejectAllPending();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { createIpcServer, createIpcClient };
|