@interactive-inc/claude-funnel 0.10.0 → 0.10.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/dist/bin.js +448 -448
- package/dist/connectors/slack.d.ts +1 -29
- package/dist/gateway/daemon.js +166 -166
- package/dist/index.d.ts +4 -11
- package/dist/index.js +133 -120
- package/dist/slack-event-processor-CS-bAit9.d.ts +43 -0
- package/package.json +1 -6
- package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
- package/lib/bin.ts +0 -3
- package/lib/cli/factory.ts +0 -10
- package/lib/cli/index.ts +0 -85
- package/lib/cli/router/query-to-cli-args.ts +0 -20
- package/lib/cli/router/to-request.ts +0 -113
- package/lib/cli/router/validator.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
- package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
- package/lib/cli/routes/channels.$channel.publish.ts +0 -52
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
- package/lib/cli/routes/channels.$channel.ts +0 -34
- package/lib/cli/routes/channels.add.$channel.ts +0 -33
- package/lib/cli/routes/channels.remove.$channel.ts +0 -20
- package/lib/cli/routes/channels.ts +0 -39
- package/lib/cli/routes/claude.ts +0 -70
- package/lib/cli/routes/gateway.listeners.ts +0 -41
- package/lib/cli/routes/gateway.logs.ts +0 -123
- package/lib/cli/routes/gateway.restart.ts +0 -50
- package/lib/cli/routes/gateway.run.ts +0 -41
- package/lib/cli/routes/gateway.start.ts +0 -50
- package/lib/cli/routes/gateway.status.ts +0 -19
- package/lib/cli/routes/gateway.stop.ts +0 -32
- package/lib/cli/routes/gateway.ts +0 -55
- package/lib/cli/routes/index.ts +0 -219
- package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
- package/lib/cli/routes/profiles.$profile.run.ts +0 -36
- package/lib/cli/routes/profiles.add.$profile.ts +0 -49
- package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
- package/lib/cli/routes/profiles.set.$profile.ts +0 -45
- package/lib/cli/routes/profiles.ts +0 -40
- package/lib/cli/routes/status.ts +0 -93
- package/lib/cli/routes/update.ts +0 -27
- package/lib/connectors/connector-adapter.ts +0 -9
- package/lib/connectors/connector-config-schema.ts +0 -16
- package/lib/connectors/connector-factory.ts +0 -94
- package/lib/connectors/connector-listener.ts +0 -20
- package/lib/connectors/discord-adapter.ts +0 -51
- package/lib/connectors/discord-connector-schema.ts +0 -12
- package/lib/connectors/discord-event-processor.ts +0 -48
- package/lib/connectors/discord-listener.ts +0 -111
- package/lib/connectors/discord.ts +0 -4
- package/lib/connectors/gh-adapter.ts +0 -48
- package/lib/connectors/gh-connector-schema.ts +0 -12
- package/lib/connectors/gh-listener.ts +0 -137
- package/lib/connectors/gh.ts +0 -3
- package/lib/connectors/match-cron.ts +0 -78
- package/lib/connectors/schedule-connector-schema.ts +0 -33
- package/lib/connectors/schedule-listener.ts +0 -207
- package/lib/connectors/schedule-state-store.ts +0 -54
- package/lib/connectors/schedule.ts +0 -4
- package/lib/connectors/slack-adapter.ts +0 -36
- package/lib/connectors/slack-connector-schema.ts +0 -13
- package/lib/connectors/slack-event-processor.ts +0 -97
- package/lib/connectors/slack-listener.ts +0 -97
- package/lib/connectors/slack.ts +0 -4
- package/lib/engine/channels/channels.ts +0 -520
- package/lib/engine/claude/claude.ts +0 -205
- package/lib/engine/claude/gateway-controller.ts +0 -4
- package/lib/engine/fs/file-system.ts +0 -23
- package/lib/engine/fs/memory-file-system.ts +0 -102
- package/lib/engine/fs/node-file-system.ts +0 -68
- package/lib/engine/http/http-client.ts +0 -17
- package/lib/engine/http/memory-http-client.ts +0 -36
- package/lib/engine/http/node-http-client.ts +0 -23
- package/lib/engine/id/id-generator.ts +0 -7
- package/lib/engine/id/memory-id-generator.ts +0 -20
- package/lib/engine/id/node-id-generator.ts +0 -7
- package/lib/engine/logger/logger.ts +0 -11
- package/lib/engine/logger/memory-logger.ts +0 -28
- package/lib/engine/logger/node-logger.ts +0 -49
- package/lib/engine/logger/noop-logger.ts +0 -9
- package/lib/engine/mcp/channel-server.ts +0 -123
- package/lib/engine/mcp/channel-subscriber.ts +0 -82
- package/lib/engine/mcp/mcp.ts +0 -126
- package/lib/engine/mcp/read-channel-connectors.ts +0 -34
- package/lib/engine/mcp/read-gateway-token.ts +0 -16
- package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
- package/lib/engine/process/memory-process-runner.ts +0 -88
- package/lib/engine/process/node-process-runner.ts +0 -91
- package/lib/engine/process/process-runner.ts +0 -33
- package/lib/engine/profiles/profile-channel-checker.ts +0 -7
- package/lib/engine/profiles/profiles.ts +0 -126
- package/lib/engine/settings/mock-settings-reader.ts +0 -27
- package/lib/engine/settings/settings-reader.ts +0 -6
- package/lib/engine/settings/settings-schema.ts +0 -48
- package/lib/engine/settings/settings-store.ts +0 -110
- package/lib/engine/time/clock.ts +0 -15
- package/lib/engine/time/memory-clock.ts +0 -26
- package/lib/engine/time/node-clock.ts +0 -7
- package/lib/funnel.ts +0 -294
- package/lib/gateway/auth-middleware.ts +0 -44
- package/lib/gateway/broadcaster.ts +0 -319
- package/lib/gateway/channel-publisher.ts +0 -67
- package/lib/gateway/daemon.ts +0 -47
- package/lib/gateway/factory.ts +0 -10
- package/lib/gateway/funnel-event-store.ts +0 -155
- package/lib/gateway/gateway-server.ts +0 -426
- package/lib/gateway/gateway-token.ts +0 -79
- package/lib/gateway/gateway.ts +0 -209
- package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
- package/lib/gateway/listener-supervisor.ts +0 -339
- package/lib/gateway/listeners-client.ts +0 -128
- package/lib/gateway/publish-schema.ts +0 -27
- package/lib/gateway/resolve-daemon-script.ts +0 -26
- package/lib/gateway/routes/channels.connectors.call.ts +0 -39
- package/lib/gateway/routes/channels.publish.ts +0 -44
- package/lib/gateway/routes/health.ts +0 -13
- package/lib/gateway/routes/index.ts +0 -26
- package/lib/gateway/routes/listeners.list.ts +0 -6
- package/lib/gateway/routes/listeners.restart.ts +0 -15
- package/lib/gateway/routes/listeners.start.ts +0 -15
- package/lib/gateway/routes/listeners.stop.ts +0 -15
- package/lib/gateway/routes/route-deps.ts +0 -19
- package/lib/gateway/routes/status.ts +0 -15
- package/lib/gateway/routes/validator.ts +0 -17
- package/lib/index.ts +0 -67
- package/lib/logger/leuco-human-file-writer.ts +0 -65
- package/lib/logger/leuco-human-logger.ts +0 -98
- package/lib/logger/leuco-human-record.ts +0 -16
- package/lib/logger/leuco-human-stdout-writer.ts +0 -26
- package/lib/logger/leuco-human-writer.ts +0 -14
- package/lib/logger/leuco-logger-memory-sink.ts +0 -67
- package/lib/logger/leuco-logger-record.ts +0 -13
- package/lib/logger/leuco-logger-sink.ts +0 -33
- package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
- package/lib/logger/leuco-logger.ts +0 -135
- package/lib/tui/app.tsx +0 -357
- package/lib/tui/components/add-row.tsx +0 -18
- package/lib/tui/components/brand.tsx +0 -27
- package/lib/tui/components/card.tsx +0 -44
- package/lib/tui/components/detail-bar.tsx +0 -46
- package/lib/tui/components/editable-field.tsx +0 -33
- package/lib/tui/components/empty-state.tsx +0 -11
- package/lib/tui/components/gateway-status.tsx +0 -66
- package/lib/tui/components/keymap.tsx +0 -29
- package/lib/tui/components/menu-item.tsx +0 -73
- package/lib/tui/components/menu.tsx +0 -26
- package/lib/tui/components/panel-header.tsx +0 -22
- package/lib/tui/components/readonly-field.tsx +0 -18
- package/lib/tui/components/section-header.tsx +0 -25
- package/lib/tui/components/selection-accent.tsx +0 -32
- package/lib/tui/components/session-item.tsx +0 -33
- package/lib/tui/components/session-list.tsx +0 -33
- package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
- package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
- package/lib/tui/components/ui/hascii/badge.tsx +0 -51
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
- package/lib/tui/components/ui/hascii/button.tsx +0 -194
- package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/card.tsx +0 -27
- package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
- package/lib/tui/components/ui/hascii/command.tsx +0 -159
- package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
- package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
- package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
- package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
- package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
- package/lib/tui/components/ui/hascii/input.tsx +0 -130
- package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
- package/lib/tui/components/ui/hascii/progress.tsx +0 -28
- package/lib/tui/components/ui/hascii/select.tsx +0 -131
- package/lib/tui/components/ui/hascii/separator.tsx +0 -35
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
- package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
- package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
- package/lib/tui/components/ui/hascii/slider.tsx +0 -91
- package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
- package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
- package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
- package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
- package/lib/tui/components/ui/hascii/switch.tsx +0 -66
- package/lib/tui/components/ui/hascii/table.tsx +0 -95
- package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
- package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
- package/lib/tui/components/ui/hascii/tree.tsx +0 -104
- package/lib/tui/components/view-shell.tsx +0 -44
- package/lib/tui/filter-input.tsx +0 -33
- package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
- package/lib/tui/parse-comma-list.ts +0 -14
- package/lib/tui/profile-launcher.tsx +0 -61
- package/lib/tui/scrollbar-options.ts +0 -19
- package/lib/tui/sidebar.tsx +0 -50
- package/lib/tui/theme.ts +0 -40
- package/lib/tui/tui.tsx +0 -20
- package/lib/tui/types.ts +0 -38
- package/lib/tui/unique-name.ts +0 -18
- package/lib/tui/use-event-stream.ts +0 -133
- package/lib/tui/use-snapshot.ts +0 -99
- package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
- package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
- package/lib/tui/utils/hascii/theme-context.tsx +0 -26
- package/lib/tui/utils/hascii/theme.ts +0 -176
- package/lib/tui/views/channels-view.tsx +0 -108
- package/lib/tui/views/connectors-view.tsx +0 -164
- package/lib/tui/views/events-view.tsx +0 -160
- package/lib/tui/views/listeners-view.tsx +0 -80
- package/lib/tui/views/profiles-view.tsx +0 -152
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "node:fs"
|
|
2
|
-
import { join } from "node:path"
|
|
3
|
-
import { type Server, type ServerWebSocket } from "bun"
|
|
4
|
-
import type { Hono } from "hono"
|
|
5
|
-
import type { FunnelChannels } from "@/engine/channels/channels"
|
|
6
|
-
import { constantTimeEqual, requireBearerToken } from "@/gateway/auth-middleware"
|
|
7
|
-
import { type Env, factory } from "@/gateway/factory"
|
|
8
|
-
import { FunnelBroadcaster } from "@/gateway/broadcaster"
|
|
9
|
-
import { FunnelEventStore } from "@/gateway/funnel-event-store"
|
|
10
|
-
import { FunnelListenerSupervisor } from "@/gateway/listener-supervisor"
|
|
11
|
-
import { killCompetingSlackGateways } from "@/gateway/kill-competing-slack-gateways"
|
|
12
|
-
import { gatewayRoutes } from "@/gateway/routes"
|
|
13
|
-
import { FunnelLogger } from "@/engine/logger/logger"
|
|
14
|
-
import { NodeFunnelLogger } from "@/engine/logger/node-logger"
|
|
15
|
-
import type { FunnelProcessRunner } from "@/engine/process/process-runner"
|
|
16
|
-
import type { FunnelSettingsReader } from "@/engine/settings/settings-reader"
|
|
17
|
-
import type { FunnelClock } from "@/engine/time/clock"
|
|
18
|
-
|
|
19
|
-
const DEFAULT_PORT = 9742
|
|
20
|
-
const DEFAULT_LOG_DIR = "/tmp/funnel/events"
|
|
21
|
-
const DB_FILENAME = "events.db"
|
|
22
|
-
|
|
23
|
-
type Deps = {
|
|
24
|
-
channels: FunnelChannels
|
|
25
|
-
settings: FunnelSettingsReader
|
|
26
|
-
port?: number
|
|
27
|
-
/** Directory holding the SQLite event store. The DB file lives at `<logDir>/events.db`. */
|
|
28
|
-
logDir?: string
|
|
29
|
-
process?: FunnelProcessRunner
|
|
30
|
-
clock?: FunnelClock
|
|
31
|
-
logger?: FunnelLogger
|
|
32
|
-
selfPid?: number
|
|
33
|
-
killCompetingSlack?: boolean
|
|
34
|
-
/** Bearer token required for `/listeners*`, `/status`, and `/ws`. Empty string disables auth (tests only). */
|
|
35
|
-
token?: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type WsData = {
|
|
39
|
-
/** Stable channel id (uuid) the client subscribed to. "" for tap-all clients. */
|
|
40
|
-
channel: string
|
|
41
|
-
/** Resolved channel name (for log readability). null for tap-all or unknown. */
|
|
42
|
-
channelName: string | null
|
|
43
|
-
/** Connector names belonging to that channel; used by tap-all replay filtering. */
|
|
44
|
-
connectors: string[]
|
|
45
|
-
tapAll?: boolean
|
|
46
|
-
/** Routing mode for this channel; resolved at upgrade time from settings. */
|
|
47
|
-
delivery: "fanout" | "exclusive"
|
|
48
|
-
/** Replay any events with offset strictly greater than this on open, then resume the live stream. */
|
|
49
|
-
since?: number
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const defaultLogger = new NodeFunnelLogger()
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* In-process gateway: runs `Bun.serve` (HTTP + WebSocket /ws), boots connector
|
|
56
|
-
* listeners through `FunnelListenerSupervisor`, fans events out via
|
|
57
|
-
* `FunnelBroadcaster`, and persists them via `FunnelEventStore` (SQLite).
|
|
58
|
-
* System events (gateway lifecycle, connect/disconnect) flow to `FunnelLogger`
|
|
59
|
-
* instead — keeping the SQLite seq space exclusive to broadcaster traffic so
|
|
60
|
-
* the broadcaster's offset counter and `getMaxSeq()` stay aligned without
|
|
61
|
-
* per-event coordination. Exposes `/listeners` HTTP for runtime
|
|
62
|
-
* start/stop/restart of individual connectors.
|
|
63
|
-
*/
|
|
64
|
-
export class FunnelGatewayServer {
|
|
65
|
-
private readonly channels: FunnelChannels
|
|
66
|
-
private readonly settings: FunnelSettingsReader
|
|
67
|
-
private readonly port: number
|
|
68
|
-
private readonly logDir: string
|
|
69
|
-
private readonly process?: FunnelProcessRunner
|
|
70
|
-
private readonly logger: FunnelLogger
|
|
71
|
-
private readonly selfPid: number
|
|
72
|
-
private readonly killCompetingSlack: boolean
|
|
73
|
-
private readonly token: string
|
|
74
|
-
private readonly broadcaster: FunnelBroadcaster
|
|
75
|
-
private readonly eventStore: FunnelEventStore
|
|
76
|
-
private readonly supervisor: FunnelListenerSupervisor
|
|
77
|
-
private readonly nowMs: () => number
|
|
78
|
-
private startedAt: number | null = null
|
|
79
|
-
private server: Server<WsData> | null = null
|
|
80
|
-
|
|
81
|
-
constructor(deps: Deps) {
|
|
82
|
-
this.channels = deps.channels
|
|
83
|
-
this.settings = deps.settings
|
|
84
|
-
this.port = deps.port ?? DEFAULT_PORT
|
|
85
|
-
this.logDir = deps.logDir ?? DEFAULT_LOG_DIR
|
|
86
|
-
this.process = deps.process
|
|
87
|
-
this.logger = deps.logger ?? defaultLogger
|
|
88
|
-
this.selfPid = deps.selfPid ?? globalThis.process.pid
|
|
89
|
-
this.killCompetingSlack = deps.killCompetingSlack ?? true
|
|
90
|
-
this.token = deps.token ?? ""
|
|
91
|
-
const clock = deps.clock
|
|
92
|
-
this.nowMs = clock ? () => clock.millis() : () => Date.now()
|
|
93
|
-
if (!existsSync(this.logDir)) mkdirSync(this.logDir, { recursive: true })
|
|
94
|
-
this.eventStore = new FunnelEventStore({
|
|
95
|
-
path: join(this.logDir, DB_FILENAME),
|
|
96
|
-
now: this.nowMs,
|
|
97
|
-
})
|
|
98
|
-
this.broadcaster = new FunnelBroadcaster({
|
|
99
|
-
logger: this.logger,
|
|
100
|
-
now: this.nowMs,
|
|
101
|
-
persistentReplay: this.eventStore,
|
|
102
|
-
})
|
|
103
|
-
this.broadcaster.seedLatestOffset(this.eventStore.findMaxOffset())
|
|
104
|
-
this.supervisor = new FunnelListenerSupervisor({
|
|
105
|
-
channels: this.channels,
|
|
106
|
-
logger: this.logger,
|
|
107
|
-
notify: async (channelName, connectorName, content, meta) => {
|
|
108
|
-
this.emit({ channel: channelName, connector: connectorName, content, meta })
|
|
109
|
-
},
|
|
110
|
-
now: this.nowMs,
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async start(): Promise<Server<WsData>> {
|
|
115
|
-
if (this.server) return this.server
|
|
116
|
-
|
|
117
|
-
const app = this.buildApp()
|
|
118
|
-
|
|
119
|
-
this.startedAt = this.nowMs()
|
|
120
|
-
this.server = Bun.serve<WsData>({
|
|
121
|
-
port: this.port,
|
|
122
|
-
development: false,
|
|
123
|
-
fetch: (request, server) => this.handleFetch(request, server, app),
|
|
124
|
-
websocket: {
|
|
125
|
-
open: (ws) => this.handleWsOpen(ws),
|
|
126
|
-
close: (ws) => this.handleWsClose(ws),
|
|
127
|
-
message() {
|
|
128
|
-
// required by Bun's websocket interface; no client → gateway messages today
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
this.logServerStarted()
|
|
134
|
-
await this.bootListeners()
|
|
135
|
-
|
|
136
|
-
return this.server
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async stop(): Promise<void> {
|
|
140
|
-
await this.supervisor.stopAll()
|
|
141
|
-
|
|
142
|
-
if (this.server) {
|
|
143
|
-
this.server.stop()
|
|
144
|
-
this.server = null
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
getStatus(): { clients: number; channels: { channel: string; connectors: string[] }[] } {
|
|
149
|
-
return {
|
|
150
|
-
clients: this.broadcaster.getClientCount(),
|
|
151
|
-
channels: this.broadcaster.listChannels(),
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
getBroadcaster(): FunnelBroadcaster {
|
|
156
|
-
return this.broadcaster
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
getSupervisor(): FunnelListenerSupervisor {
|
|
160
|
-
return this.supervisor
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
getEventStore(): FunnelEventStore {
|
|
164
|
-
return this.eventStore
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private handleFetch(
|
|
168
|
-
request: Request,
|
|
169
|
-
server: Server<WsData>,
|
|
170
|
-
app: Hono<Env>,
|
|
171
|
-
): Response | Promise<Response> | undefined {
|
|
172
|
-
const url = new URL(request.url)
|
|
173
|
-
|
|
174
|
-
if (url.pathname === "/ws" && request.headers.get("upgrade") === "websocket") {
|
|
175
|
-
if (this.token && !this.tokenMatchesUpgrade(request)) {
|
|
176
|
-
return new Response("unauthorized", { status: 401 })
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const tapAll = url.searchParams.get("tap") === "all"
|
|
180
|
-
const requestedChannel = tapAll ? "" : (url.searchParams.get("channel") ?? "")
|
|
181
|
-
const channel = !tapAll && requestedChannel ? this.resolveChannel(requestedChannel) : null
|
|
182
|
-
const channelId = tapAll ? "" : (channel?.id ?? requestedChannel)
|
|
183
|
-
const channelName = tapAll ? null : (channel?.name ?? null)
|
|
184
|
-
const connectors = channel?.connectors ?? []
|
|
185
|
-
const delivery = channel?.delivery ?? "fanout"
|
|
186
|
-
const sinceRaw = url.searchParams.get("since")
|
|
187
|
-
const sinceParsed = sinceRaw === null ? Number.NaN : Number.parseInt(sinceRaw, 10)
|
|
188
|
-
const since = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : undefined
|
|
189
|
-
const upgraded = server.upgrade(request, {
|
|
190
|
-
data: {
|
|
191
|
-
channel: channelId,
|
|
192
|
-
channelName,
|
|
193
|
-
connectors,
|
|
194
|
-
tapAll,
|
|
195
|
-
delivery,
|
|
196
|
-
since,
|
|
197
|
-
},
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
if (upgraded) return undefined
|
|
201
|
-
|
|
202
|
-
return new Response("WebSocket upgrade failed", { status: 400 })
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return app.fetch(request)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private handleWsOpen(ws: ServerWebSocket<WsData>): void {
|
|
209
|
-
if (typeof ws.data.since === "number") {
|
|
210
|
-
const replay = this.broadcaster.replaySince(ws.data.since, ws.data)
|
|
211
|
-
|
|
212
|
-
for (const event of replay) ws.send(JSON.stringify(event))
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
this.broadcaster.addClient(ws, ws.data)
|
|
216
|
-
|
|
217
|
-
if (ws.data.channelName) {
|
|
218
|
-
const meta: Record<string, string> = {
|
|
219
|
-
event_type: "system",
|
|
220
|
-
action: "channel_connect",
|
|
221
|
-
channel: ws.data.channelName,
|
|
222
|
-
channelId: ws.data.channel,
|
|
223
|
-
connectors: ws.data.connectors.join(","),
|
|
224
|
-
total: String(this.broadcaster.getClientCount()),
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
this.logger.info("channel connected", meta)
|
|
228
|
-
} else {
|
|
229
|
-
this.logger.info("tap-all client connected", {
|
|
230
|
-
event_type: "system",
|
|
231
|
-
action: "tap_connect",
|
|
232
|
-
total: String(this.broadcaster.getClientCount()),
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private handleWsClose(ws: ServerWebSocket<WsData>): void {
|
|
238
|
-
this.broadcaster.removeClient(ws)
|
|
239
|
-
|
|
240
|
-
if (ws.data.channelName) {
|
|
241
|
-
this.logger.info("channel disconnected", {
|
|
242
|
-
event_type: "system",
|
|
243
|
-
action: "channel_disconnect",
|
|
244
|
-
channel: ws.data.channelName,
|
|
245
|
-
channelId: ws.data.channel,
|
|
246
|
-
total: String(this.broadcaster.getClientCount()),
|
|
247
|
-
})
|
|
248
|
-
} else {
|
|
249
|
-
this.logger.info("tap-all client disconnected", {
|
|
250
|
-
event_type: "system",
|
|
251
|
-
action: "tap_disconnect",
|
|
252
|
-
total: String(this.broadcaster.getClientCount()),
|
|
253
|
-
})
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private logServerStarted(): void {
|
|
258
|
-
this.logger.info("gateway started", {
|
|
259
|
-
event_type: "system",
|
|
260
|
-
action: "gateway_start",
|
|
261
|
-
port: String(this.port),
|
|
262
|
-
pid: String(this.selfPid),
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
this.logger.info("funnel gateway listening", {
|
|
266
|
-
url: `http://localhost:${this.port}`,
|
|
267
|
-
websocket: `ws://localhost:${this.port}/ws`,
|
|
268
|
-
health: `http://localhost:${this.port}/health`,
|
|
269
|
-
})
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private buildApp(): Hono<Env> {
|
|
273
|
-
const base = factory.createApp()
|
|
274
|
-
|
|
275
|
-
base.use((c, next) => {
|
|
276
|
-
c.set("deps", {
|
|
277
|
-
selfPid: this.selfPid,
|
|
278
|
-
broadcaster: this.broadcaster,
|
|
279
|
-
supervisor: this.supervisor,
|
|
280
|
-
channels: this.channels,
|
|
281
|
-
uptimeMs: () => (this.startedAt ? this.nowMs() - this.startedAt : 0),
|
|
282
|
-
emit: (input) => this.emit(input),
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
return next()
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
if (this.token) {
|
|
289
|
-
base.use("/listeners/*", requireBearerToken({ expected: this.token }))
|
|
290
|
-
base.use("/status", requireBearerToken({ expected: this.token }))
|
|
291
|
-
base.use("/channels/*", requireBearerToken({ expected: this.token }))
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return base.route("/", gatewayRoutes)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Reads the bearer token from the WebSocket upgrade request. Accepts:
|
|
299
|
-
* - `Sec-WebSocket-Protocol: funnel.token.<value>` (preferred — header, never logged in URLs)
|
|
300
|
-
* - `Authorization: Bearer <value>` (also header-based)
|
|
301
|
-
* Returns true on a constant-time match against the daemon token.
|
|
302
|
-
*/
|
|
303
|
-
private tokenMatchesUpgrade(request: Request): boolean {
|
|
304
|
-
const protocols = (request.headers.get("sec-websocket-protocol") ?? "")
|
|
305
|
-
.split(",")
|
|
306
|
-
.map((p) => p.trim())
|
|
307
|
-
.filter((p) => p.length > 0)
|
|
308
|
-
|
|
309
|
-
for (const proto of protocols) {
|
|
310
|
-
if (
|
|
311
|
-
proto.startsWith("funnel.token.") &&
|
|
312
|
-
constantTimeEqual(proto.slice("funnel.token.".length), this.token)
|
|
313
|
-
) {
|
|
314
|
-
return true
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const auth = request.headers.get("authorization") ?? ""
|
|
319
|
-
const match = auth.match(/^Bearer\s+(.+)$/i)
|
|
320
|
-
|
|
321
|
-
if (match && constantTimeEqual(match[1] ?? "", this.token)) return true
|
|
322
|
-
|
|
323
|
-
return false
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private resolveChannel(
|
|
327
|
-
requested: string,
|
|
328
|
-
): { id: string; name: string; connectors: string[]; delivery: "fanout" | "exclusive" } | null {
|
|
329
|
-
const settings = this.settings.read()
|
|
330
|
-
const channel = settings?.channels.find((c) => c.id === requested || c.name === requested)
|
|
331
|
-
|
|
332
|
-
if (!channel) return null
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
id: channel.id,
|
|
336
|
-
name: channel.name,
|
|
337
|
-
connectors: channel.connectors.map((c) => c.name),
|
|
338
|
-
delivery: channel.delivery,
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
private async bootListeners(): Promise<void> {
|
|
343
|
-
const allConnectors = this.channels.listAllConnectors()
|
|
344
|
-
|
|
345
|
-
if (this.killCompetingSlack && allConnectors.some((c) => c.type === "slack")) {
|
|
346
|
-
const killed = await killCompetingSlackGateways({
|
|
347
|
-
selfPid: this.selfPid,
|
|
348
|
-
process: this.process,
|
|
349
|
-
logger: this.logger,
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
if (killed.length > 0) {
|
|
353
|
-
this.logger.info("killed competing Slack gateway processes", {
|
|
354
|
-
event_type: "system",
|
|
355
|
-
action: "kill_competing",
|
|
356
|
-
pids: killed.join(","),
|
|
357
|
-
})
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
await this.supervisor.startAll()
|
|
362
|
-
|
|
363
|
-
for (const entry of this.supervisor.list()) {
|
|
364
|
-
this.logger.info(`${entry.type} listener started: ${entry.name}`, {
|
|
365
|
-
event_type: "system",
|
|
366
|
-
action: `${entry.type}_connect`,
|
|
367
|
-
channel: entry.channelName,
|
|
368
|
-
connector: entry.name,
|
|
369
|
-
})
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
this.logger.info(`event store: ${join(this.logDir, DB_FILENAME)}`)
|
|
373
|
-
this.logger.info("funnel gateway running")
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Broadcast `content` to subscribers of `channel`, persisting the event in
|
|
378
|
-
* the SQLite store and stamping `meta.channel{,Id}` / `meta.connector{,Id}`
|
|
379
|
-
* when they resolve. Used by both the connector-listener path (via the
|
|
380
|
-
* supervisor's `notify` callback) and the public `/channels/:channel/publish`
|
|
381
|
-
* route. Returns the assigned event offset.
|
|
382
|
-
*/
|
|
383
|
-
emit(input: {
|
|
384
|
-
channel: string
|
|
385
|
-
connector?: string
|
|
386
|
-
content: string
|
|
387
|
-
meta?: Record<string, string>
|
|
388
|
-
}): { offset: number } {
|
|
389
|
-
const channelId = this.lookupChannelId(input.channel)
|
|
390
|
-
const connectorId =
|
|
391
|
-
channelId && input.connector ? this.lookupConnectorId(channelId, input.connector) : null
|
|
392
|
-
const enriched: Record<string, string> = {
|
|
393
|
-
...input.meta,
|
|
394
|
-
channel: input.channel,
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (input.connector) enriched.connector = input.connector
|
|
398
|
-
if (channelId) enriched.channelId = channelId
|
|
399
|
-
if (connectorId) enriched.connectorId = connectorId
|
|
400
|
-
|
|
401
|
-
const event = this.broadcaster.broadcast(input.content, enriched)
|
|
402
|
-
|
|
403
|
-
this.eventStore.record({
|
|
404
|
-
content: input.content,
|
|
405
|
-
channelId: channelId ?? null,
|
|
406
|
-
connectorId: connectorId ?? null,
|
|
407
|
-
meta: enriched,
|
|
408
|
-
offset: event.offset,
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
return { offset: event.offset }
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
private lookupChannelId(channelName: string): string | null {
|
|
415
|
-
const channel = this.settings.read().channels.find((c) => c.name === channelName)
|
|
416
|
-
|
|
417
|
-
return channel?.id ?? null
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private lookupConnectorId(channelId: string, connectorName: string): string | null {
|
|
421
|
-
const channel = this.settings.read().channels.find((c) => c.id === channelId)
|
|
422
|
-
const connector = channel?.connectors.find((c) => c.name === connectorName)
|
|
423
|
-
|
|
424
|
-
return connector?.id ?? null
|
|
425
|
-
}
|
|
426
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os"
|
|
2
|
-
import { dirname, join } from "node:path"
|
|
3
|
-
import { FunnelFileSystem } from "@/engine/fs/file-system"
|
|
4
|
-
import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
|
|
5
|
-
import { FUNNEL_DIR } from "@/engine/settings/settings-store"
|
|
6
|
-
|
|
7
|
-
const TOKEN_FILE_NAME = "gateway.token"
|
|
8
|
-
const TOKEN_BYTES = 32
|
|
9
|
-
|
|
10
|
-
type Deps = {
|
|
11
|
-
fs?: FunnelFileSystem
|
|
12
|
-
dir?: string
|
|
13
|
-
generate?: () => string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const defaultFs = new NodeFunnelFileSystem()
|
|
17
|
-
|
|
18
|
-
const defaultGenerate = (): string => {
|
|
19
|
-
const buf = new Uint8Array(TOKEN_BYTES)
|
|
20
|
-
crypto.getRandomValues(buf)
|
|
21
|
-
|
|
22
|
-
return [...buf].map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Reads / generates the gateway daemon token used to authenticate
|
|
27
|
-
* `/listeners*`, `/status`, and `/ws` connections.
|
|
28
|
-
*
|
|
29
|
-
* Token file: `<dir>/gateway.token` (default `~/.funnel/gateway.token`),
|
|
30
|
-
* written with mode 0600. Clients on the same machine as the daemon read
|
|
31
|
-
* the file directly; the token never leaves the user's home directory.
|
|
32
|
-
*/
|
|
33
|
-
export class FunnelGatewayToken {
|
|
34
|
-
private readonly fs: FunnelFileSystem
|
|
35
|
-
private readonly path: string
|
|
36
|
-
private readonly generate: () => string
|
|
37
|
-
|
|
38
|
-
constructor(deps: Deps = {}) {
|
|
39
|
-
this.fs = deps.fs ?? defaultFs
|
|
40
|
-
this.path = join(deps.dir ?? FUNNEL_DIR, TOKEN_FILE_NAME)
|
|
41
|
-
this.generate = deps.generate ?? defaultGenerate
|
|
42
|
-
Object.freeze(this)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
read(): string | null {
|
|
46
|
-
if (!this.fs.existsSync(this.path)) return null
|
|
47
|
-
|
|
48
|
-
const value = this.fs.readFileSync(this.path).trim()
|
|
49
|
-
|
|
50
|
-
return value.length > 0 ? value : null
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Returns the existing token or, if missing, generates one and writes it with mode 0600.
|
|
55
|
-
*
|
|
56
|
-
* NOTE: not atomic — two concurrent `ensure()` calls (e.g., `fnl gateway start` racing
|
|
57
|
-
* itself before the PID lock is acquired) could each generate independent tokens. The
|
|
58
|
-
* gateway PID file makes this practically a non-issue; if you need stronger guarantees,
|
|
59
|
-
* take a file lock around this call externally.
|
|
60
|
-
*/
|
|
61
|
-
ensure(): string {
|
|
62
|
-
const existing = this.read()
|
|
63
|
-
|
|
64
|
-
if (existing) return existing
|
|
65
|
-
|
|
66
|
-
const token = this.generate()
|
|
67
|
-
|
|
68
|
-
this.fs.mkdirSync(dirname(this.path), { recursive: true })
|
|
69
|
-
this.fs.writeSecretFileSync(this.path, `${token}\n`)
|
|
70
|
-
|
|
71
|
-
return token
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
getPath(): string {
|
|
75
|
-
return this.path
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export const DEFAULT_GATEWAY_TOKEN_PATH = join(homedir(), ".funnel", TOKEN_FILE_NAME)
|
package/lib/gateway/gateway.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path"
|
|
2
|
-
import { FunnelFileSystem } from "@/engine/fs/file-system"
|
|
3
|
-
import { resolveDaemonScript } from "@/gateway/resolve-daemon-script"
|
|
4
|
-
import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
|
|
5
|
-
import { FunnelProcessRunner } from "@/engine/process/process-runner"
|
|
6
|
-
import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
|
|
7
|
-
import { FUNNEL_DIR } from "@/engine/settings/settings-store"
|
|
8
|
-
import { FunnelClock } from "@/engine/time/clock"
|
|
9
|
-
import { NodeFunnelClock } from "@/engine/time/node-clock"
|
|
10
|
-
|
|
11
|
-
const DEFAULT_PORT = 9742
|
|
12
|
-
const DEFAULT_TMP_DIR = "/tmp/funnel"
|
|
13
|
-
const STARTUP_TIMEOUT_MS = 5000
|
|
14
|
-
const SIGTERM_TIMEOUT_MS = 2000
|
|
15
|
-
const POLL_INTERVAL_MS = 100
|
|
16
|
-
const SIGKILL_GRACE_MS = 200
|
|
17
|
-
|
|
18
|
-
type Deps = {
|
|
19
|
-
process?: FunnelProcessRunner
|
|
20
|
-
fs?: FunnelFileSystem
|
|
21
|
-
clock?: FunnelClock
|
|
22
|
-
dir?: string
|
|
23
|
-
tmpDir?: string
|
|
24
|
-
port?: number
|
|
25
|
-
sleep?: (ms: number) => Promise<void>
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const defaultProcess = new NodeFunnelProcessRunner()
|
|
29
|
-
const defaultFs = new NodeFunnelFileSystem()
|
|
30
|
-
const defaultClock = new NodeFunnelClock()
|
|
31
|
-
const defaultSleep = (ms: number): Promise<void> =>
|
|
32
|
-
new Promise((r) => {
|
|
33
|
-
setTimeout(r, ms)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Manages the gateway daemon as a separate process via PID file.
|
|
38
|
-
* Use `start()` to spawn `bun daemon.ts` in the background and `stop()` to
|
|
39
|
-
* terminate it. For an in-process gateway, use `Funnel.gatewayServer` instead.
|
|
40
|
-
*/
|
|
41
|
-
export class FunnelGateway {
|
|
42
|
-
private readonly process: FunnelProcessRunner
|
|
43
|
-
private readonly fs: FunnelFileSystem
|
|
44
|
-
private readonly clock: FunnelClock
|
|
45
|
-
private readonly pidFile: string
|
|
46
|
-
private readonly logDir: string
|
|
47
|
-
private readonly gatewayLog: string
|
|
48
|
-
private readonly tmpDir: string
|
|
49
|
-
private readonly port: number
|
|
50
|
-
private readonly sleep: (ms: number) => Promise<void>
|
|
51
|
-
|
|
52
|
-
constructor(deps: Deps = {}) {
|
|
53
|
-
this.process = deps.process ?? defaultProcess
|
|
54
|
-
this.fs = deps.fs ?? defaultFs
|
|
55
|
-
this.clock = deps.clock ?? defaultClock
|
|
56
|
-
const baseDir = deps.dir ?? FUNNEL_DIR
|
|
57
|
-
this.tmpDir = deps.tmpDir ?? DEFAULT_TMP_DIR
|
|
58
|
-
this.pidFile = join(baseDir, "gateway.pid")
|
|
59
|
-
this.logDir = join(this.tmpDir, "events")
|
|
60
|
-
this.gatewayLog = join(this.tmpDir, "gateway.log")
|
|
61
|
-
this.port = deps.port ?? DEFAULT_PORT
|
|
62
|
-
this.sleep = deps.sleep ?? defaultSleep
|
|
63
|
-
Object.freeze(this)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
isRunning(): boolean {
|
|
67
|
-
const pid = this.readPid()
|
|
68
|
-
|
|
69
|
-
if (!pid) return false
|
|
70
|
-
|
|
71
|
-
return this.isProcessAlive(pid)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
getStatus(): { running: boolean; pid: number | null; port: number } {
|
|
75
|
-
const pid = this.readPid()
|
|
76
|
-
const running = pid !== null && this.isProcessAlive(pid)
|
|
77
|
-
|
|
78
|
-
return { running, pid: running ? pid : null, port: this.port }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async start(options: { caffeinate?: boolean } = {}): Promise<boolean> {
|
|
82
|
-
if (this.isRunning()) return true
|
|
83
|
-
|
|
84
|
-
this.fs.mkdirSync(this.tmpDir, { recursive: true })
|
|
85
|
-
|
|
86
|
-
const gatewayScript = resolveDaemonScript()
|
|
87
|
-
const command = this.buildStartCommand(gatewayScript, options)
|
|
88
|
-
|
|
89
|
-
this.process.detach(["bash", "-c", command])
|
|
90
|
-
|
|
91
|
-
const deadline = Date.now() + STARTUP_TIMEOUT_MS
|
|
92
|
-
|
|
93
|
-
while (Date.now() < deadline) {
|
|
94
|
-
if (this.isRunning()) return true
|
|
95
|
-
await this.sleep(POLL_INTERVAL_MS)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return this.isRunning()
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
buildStartCommand(gatewayScript: string, options: { caffeinate?: boolean } = {}): string {
|
|
102
|
-
const useCaffeinate = options.caffeinate !== false && globalThis.process.platform === "darwin"
|
|
103
|
-
const prefix = useCaffeinate ? "caffeinate -i " : ""
|
|
104
|
-
|
|
105
|
-
return `nohup ${prefix}bun ${gatewayScript} >> ${this.gatewayLog} 2>&1 &`
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async stop(): Promise<boolean> {
|
|
109
|
-
const pid = this.readPid()
|
|
110
|
-
|
|
111
|
-
if (!pid) return true
|
|
112
|
-
|
|
113
|
-
if (!this.isProcessAlive(pid)) {
|
|
114
|
-
this.removePid()
|
|
115
|
-
return true
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
this.process.kill(pid, "SIGTERM")
|
|
120
|
-
} catch {
|
|
121
|
-
return false
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const deadline = this.clock.millis() + SIGTERM_TIMEOUT_MS
|
|
125
|
-
|
|
126
|
-
while (this.clock.millis() < deadline) {
|
|
127
|
-
if (!this.isProcessAlive(pid)) {
|
|
128
|
-
this.removePid()
|
|
129
|
-
return true
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
await this.sleep(POLL_INTERVAL_MS)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
this.process.kill(pid, "SIGKILL")
|
|
137
|
-
} catch {
|
|
138
|
-
// ignore
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
await this.sleep(SIGKILL_GRACE_MS)
|
|
142
|
-
this.removePid()
|
|
143
|
-
|
|
144
|
-
return !this.isProcessAlive(pid)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async restart(
|
|
148
|
-
options: { onlyIfRunning?: boolean; caffeinate?: boolean } = {},
|
|
149
|
-
): Promise<{ ok: boolean; wasRunning: boolean; stopped: boolean; started: boolean }> {
|
|
150
|
-
const wasRunning = this.isRunning()
|
|
151
|
-
|
|
152
|
-
if (options.onlyIfRunning && !wasRunning) {
|
|
153
|
-
return { ok: true, wasRunning: false, stopped: false, started: false }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const stopped = wasRunning ? await this.stop() : true
|
|
157
|
-
|
|
158
|
-
if (!stopped) {
|
|
159
|
-
return { ok: false, wasRunning, stopped: false, started: false }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const started = await this.start({ caffeinate: options.caffeinate })
|
|
163
|
-
|
|
164
|
-
return { ok: started, wasRunning, stopped, started }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
getLogDir(): string {
|
|
168
|
-
return this.logDir
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
getGatewayLog(): string {
|
|
172
|
-
return this.gatewayLog
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
getPort(): number {
|
|
176
|
-
return this.port
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private readPid(): number | null {
|
|
180
|
-
if (!this.fs.existsSync(this.pidFile)) return null
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const content = this.fs.readFileSync(this.pidFile).trim()
|
|
184
|
-
const pid = Number(content)
|
|
185
|
-
|
|
186
|
-
if (!pid || pid <= 0) return null
|
|
187
|
-
|
|
188
|
-
return pid
|
|
189
|
-
} catch {
|
|
190
|
-
return null
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private removePid(): void {
|
|
195
|
-
this.fs.unlink(this.pidFile)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private isProcessAlive(pid: number): boolean {
|
|
199
|
-
const result = this.process.runSync(["ps", "-p", String(pid), "-o", "state="])
|
|
200
|
-
|
|
201
|
-
if (result.exitCode !== 0) return false
|
|
202
|
-
|
|
203
|
-
const state = result.stdout.trim()
|
|
204
|
-
|
|
205
|
-
if (!state) return false
|
|
206
|
-
|
|
207
|
-
return !state.startsWith("Z")
|
|
208
|
-
}
|
|
209
|
-
}
|