tailii-host 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -14
- package/dist/{broker.d.ts → approval/broker.d.ts} +9 -5
- package/dist/{broker.js → approval/broker.js} +42 -6
- package/dist/approval/broker.js.map +1 -0
- package/dist/{hook.d.ts → approval/hook.d.ts} +48 -7
- package/dist/{hook.js → approval/hook.js} +359 -50
- package/dist/approval/hook.js.map +1 -0
- package/dist/backend/herdr.d.ts +226 -0
- package/dist/backend/herdr.js +749 -0
- package/dist/backend/herdr.js.map +1 -0
- package/dist/backend/sessionBackend.d.ts +81 -0
- package/dist/backend/sessionBackend.js +114 -0
- package/dist/backend/sessionBackend.js.map +1 -0
- package/dist/backend/tmux.d.ts +125 -0
- package/dist/backend/tmux.js +392 -0
- package/dist/backend/tmux.js.map +1 -0
- package/dist/{chatTailController.d.ts → chat/chatTailController.d.ts} +29 -6
- package/dist/{chatTailController.js → chat/chatTailController.js} +94 -18
- package/dist/chat/chatTailController.js.map +1 -0
- package/dist/{imageService.d.ts → chat/imageService.d.ts} +12 -1
- package/dist/{imageService.js → chat/imageService.js} +39 -11
- package/dist/chat/imageService.js.map +1 -0
- package/dist/chat/remoteQuestionMonitor.d.ts +27 -0
- package/dist/chat/remoteQuestionMonitor.js +128 -0
- package/dist/chat/remoteQuestionMonitor.js.map +1 -0
- package/dist/chat/subagentTailer.d.ts +30 -0
- package/dist/chat/subagentTailer.js +704 -0
- package/dist/chat/subagentTailer.js.map +1 -0
- package/dist/chat/subagentTranscript.d.ts +20 -0
- package/dist/chat/subagentTranscript.js +273 -0
- package/dist/chat/subagentTranscript.js.map +1 -0
- package/dist/{transcriptTailer.d.ts → chat/transcriptTailer.d.ts} +12 -1
- package/dist/{transcriptTailer.js → chat/transcriptTailer.js} +183 -10
- package/dist/chat/transcriptTailer.js.map +1 -0
- package/dist/cli.js +15 -10
- package/dist/cli.js.map +1 -1
- package/dist/codex/codexAccountUsage.d.ts +22 -0
- package/dist/codex/codexAccountUsage.js +128 -0
- package/dist/codex/codexAccountUsage.js.map +1 -0
- package/dist/codex/codexAppServer.d.ts +166 -0
- package/dist/codex/codexAppServer.js +1297 -0
- package/dist/codex/codexAppServer.js.map +1 -0
- package/dist/codex/codexNativeTurnController.d.ts +153 -0
- package/dist/codex/codexNativeTurnController.js +673 -0
- package/dist/codex/codexNativeTurnController.js.map +1 -0
- package/dist/codex/codexRolloutTailer.d.ts +100 -0
- package/dist/codex/codexRolloutTailer.js +568 -0
- package/dist/codex/codexRolloutTailer.js.map +1 -0
- package/dist/{codexSessionStore.d.ts → codex/codexSessionStore.d.ts} +10 -2
- package/dist/{codexSessionStore.js → codex/codexSessionStore.js} +184 -15
- package/dist/codex/codexSessionStore.js.map +1 -0
- package/dist/codex/codexSubagentTracker.d.ts +21 -0
- package/dist/codex/codexSubagentTracker.js +281 -0
- package/dist/codex/codexSubagentTracker.js.map +1 -0
- package/dist/codex/codexToolActivity.d.ts +69 -0
- package/dist/codex/codexToolActivity.js +425 -0
- package/dist/codex/codexToolActivity.js.map +1 -0
- package/dist/{codexUsage.d.ts → codex/codexUsage.d.ts} +1 -1
- package/dist/{codexUsage.js → codex/codexUsage.js} +19 -26
- package/dist/codex/codexUsage.js.map +1 -0
- package/dist/commands/doctor.d.ts +65 -0
- package/dist/commands/doctor.js +425 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/{hookSettings.d.ts → commands/hookSettings.d.ts} +14 -1
- package/dist/{hookSettings.js → commands/hookSettings.js} +96 -12
- package/dist/commands/hookSettings.js.map +1 -0
- package/dist/{kick.js → commands/kick.js} +16 -1
- package/dist/commands/kick.js.map +1 -0
- package/dist/{launch.d.ts → commands/launch.d.ts} +67 -5
- package/dist/commands/launch.js +716 -0
- package/dist/commands/launch.js.map +1 -0
- package/dist/commands/legacyHomeMigration.js.map +1 -0
- package/dist/{pairingCode.d.ts → commands/pairingCode.d.ts} +29 -2
- package/dist/{pairingCode.js → commands/pairingCode.js} +120 -11
- package/dist/commands/pairingCode.js.map +1 -0
- package/dist/{pushTokenCommand.d.ts → commands/pushTokenCommand.d.ts} +1 -1
- package/dist/{pushTokenCommand.js → commands/pushTokenCommand.js} +2 -2
- package/dist/commands/pushTokenCommand.js.map +1 -0
- package/dist/{setup.d.ts → commands/setup.d.ts} +17 -6
- package/dist/{setup.js → commands/setup.js} +133 -75
- package/dist/commands/setup.js.map +1 -0
- package/dist/engine/context.d.ts +145 -0
- package/dist/engine/context.js +184 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/engine.d.ts +121 -0
- package/dist/engine/engine.js +799 -0
- package/dist/engine/engine.js.map +1 -0
- package/dist/engine/handlers/accountUsage.d.ts +53 -0
- package/dist/engine/handlers/accountUsage.js +203 -0
- package/dist/engine/handlers/accountUsage.js.map +1 -0
- package/dist/engine/handlers/claudeModels.d.ts +2 -0
- package/dist/engine/handlers/claudeModels.js +23 -0
- package/dist/engine/handlers/claudeModels.js.map +1 -0
- package/dist/engine/handlers/codex.d.ts +2 -0
- package/dist/engine/handlers/codex.js +97 -0
- package/dist/engine/handlers/codex.js.map +1 -0
- package/dist/engine/handlers/conversation.d.ts +2 -0
- package/dist/engine/handlers/conversation.js +259 -0
- package/dist/engine/handlers/conversation.js.map +1 -0
- package/dist/engine/handlers/core.d.ts +2 -0
- package/dist/engine/handlers/core.js +66 -0
- package/dist/engine/handlers/core.js.map +1 -0
- package/dist/engine/handlers/git.d.ts +2 -0
- package/dist/engine/handlers/git.js +127 -0
- package/dist/engine/handlers/git.js.map +1 -0
- package/dist/engine/handlers/index.d.ts +2 -0
- package/dist/engine/handlers/index.js +32 -0
- package/dist/engine/handlers/index.js.map +1 -0
- package/dist/engine/handlers/mode.d.ts +2 -0
- package/dist/engine/handlers/mode.js +194 -0
- package/dist/engine/handlers/mode.js.map +1 -0
- package/dist/engine/handlers/officialApps.d.ts +2 -0
- package/dist/engine/handlers/officialApps.js +79 -0
- package/dist/engine/handlers/officialApps.js.map +1 -0
- package/dist/engine/handlers/preview.d.ts +2 -0
- package/dist/engine/handlers/preview.js +59 -0
- package/dist/engine/handlers/preview.js.map +1 -0
- package/dist/engine/handlers/session.d.ts +2 -0
- package/dist/engine/handlers/session.js +385 -0
- package/dist/engine/handlers/session.js.map +1 -0
- package/dist/engine/handlers/usage.d.ts +2 -0
- package/dist/engine/handlers/usage.js +96 -0
- package/dist/engine/handlers/usage.js.map +1 -0
- package/dist/engine/handlers/workspace.d.ts +2 -0
- package/dist/engine/handlers/workspace.js +110 -0
- package/dist/engine/handlers/workspace.js.map +1 -0
- package/dist/engine/slashCommands.d.ts +3 -0
- package/dist/engine/slashCommands.js +211 -0
- package/dist/engine/slashCommands.js.map +1 -0
- package/dist/hub/engineRelaySocket.d.ts +58 -0
- package/dist/hub/engineRelaySocket.js +161 -0
- package/dist/hub/engineRelaySocket.js.map +1 -0
- package/dist/hub/hubClient.d.ts +28 -0
- package/dist/hub/hubClient.js +187 -0
- package/dist/hub/hubClient.js.map +1 -0
- package/dist/hub/hubDaemon.d.ts +69 -0
- package/dist/hub/hubDaemon.js +466 -0
- package/dist/hub/hubDaemon.js.map +1 -0
- package/dist/hub/hubProtocol.d.ts +161 -0
- package/dist/hub/hubProtocol.js +379 -0
- package/dist/hub/hubProtocol.js.map +1 -0
- package/dist/hub/panePreviewPump.d.ts +65 -0
- package/dist/hub/panePreviewPump.js +187 -0
- package/dist/hub/panePreviewPump.js.map +1 -0
- package/dist/hub/questionInjection.d.ts +19 -0
- package/dist/hub/questionInjection.js +126 -0
- package/dist/hub/questionInjection.js.map +1 -0
- package/dist/hub/reaper.d.ts +74 -0
- package/dist/hub/reaper.js +333 -0
- package/dist/hub/reaper.js.map +1 -0
- package/dist/hub/sessionHub.d.ts +203 -0
- package/dist/hub/sessionHub.js +1862 -0
- package/dist/hub/sessionHub.js.map +1 -0
- package/dist/protocol/common.d.ts +18 -0
- package/dist/protocol/common.js +78 -0
- package/dist/protocol/common.js.map +1 -0
- package/dist/protocol/decode.d.ts +3 -0
- package/dist/protocol/decode.js +1036 -0
- package/dist/protocol/decode.js.map +1 -0
- package/dist/protocol/encode.d.ts +3 -0
- package/dist/protocol/encode.js +84 -0
- package/dist/protocol/encode.js.map +1 -0
- package/dist/protocol/messages.d.ts +1002 -0
- package/dist/protocol/messages.js +8 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/protocol.d.ts +4 -329
- package/dist/protocol.js +13 -531
- package/dist/protocol.js.map +1 -1
- package/dist/push/apnsConfigStore.js.map +1 -0
- package/dist/push/apnsJwtProvider.js.map +1 -0
- package/dist/{apnsSender.js.map → push/apnsSender.js.map} +1 -1
- package/dist/{approvalPushNotifier.js → push/approvalPushNotifier.js} +1 -1
- package/dist/push/approvalPushNotifier.js.map +1 -0
- package/dist/push/deviceTokenStore.js.map +1 -0
- package/dist/{pushTypes.js → push/pushTypes.js} +1 -1
- package/dist/push/pushTypes.js.map +1 -0
- package/dist/push/sendLog.js.map +1 -0
- package/dist/services/accountIdentity.d.ts +44 -0
- package/dist/services/accountIdentity.js +184 -0
- package/dist/services/accountIdentity.js.map +1 -0
- package/dist/services/claudeCredentialRefresh.d.ts +30 -0
- package/dist/services/claudeCredentialRefresh.js +345 -0
- package/dist/services/claudeCredentialRefresh.js.map +1 -0
- package/dist/services/claudeModelCatalog.d.ts +21 -0
- package/dist/services/claudeModelCatalog.js +181 -0
- package/dist/services/claudeModelCatalog.js.map +1 -0
- package/dist/{dirLister.d.ts → services/dirLister.d.ts} +9 -4
- package/dist/services/dirLister.js +169 -0
- package/dist/services/dirLister.js.map +1 -0
- package/dist/services/fileService.d.ts +10 -0
- package/dist/services/fileService.js +123 -0
- package/dist/services/fileService.js.map +1 -0
- package/dist/services/gitService.d.ts +79 -0
- package/dist/services/gitService.js +648 -0
- package/dist/services/gitService.js.map +1 -0
- package/dist/services/hostVersions.d.ts +38 -0
- package/dist/services/hostVersions.js +120 -0
- package/dist/services/hostVersions.js.map +1 -0
- package/dist/services/officialApps.d.ts +148 -0
- package/dist/services/officialApps.js +1079 -0
- package/dist/services/officialApps.js.map +1 -0
- package/dist/services/planUsageFetcher.d.ts +119 -0
- package/dist/services/planUsageFetcher.js +357 -0
- package/dist/services/planUsageFetcher.js.map +1 -0
- package/dist/services/previewServer.d.ts +31 -0
- package/dist/services/previewServer.js +233 -0
- package/dist/services/previewServer.js.map +1 -0
- package/dist/services/quicGateway.d.ts +143 -0
- package/dist/services/quicGateway.js +292 -0
- package/dist/services/quicGateway.js.map +1 -0
- package/dist/services/serveService.d.ts +50 -0
- package/dist/services/serveService.js +302 -0
- package/dist/services/serveService.js.map +1 -0
- package/dist/services/usageAggregator.js.map +1 -0
- package/dist/sessions/claudeSessionStore.d.ts +80 -0
- package/dist/sessions/claudeSessionStore.js +477 -0
- package/dist/sessions/claudeSessionStore.js.map +1 -0
- package/dist/sessions/heartbeat.d.ts +24 -0
- package/dist/sessions/heartbeat.js +86 -0
- package/dist/sessions/heartbeat.js.map +1 -0
- package/dist/sessions/liveSessionJoin.d.ts +20 -0
- package/dist/sessions/liveSessionJoin.js +80 -0
- package/dist/sessions/liveSessionJoin.js.map +1 -0
- package/dist/sessions/sessionActivityProvider.d.ts +14 -0
- package/dist/sessions/sessionActivityProvider.js +57 -0
- package/dist/sessions/sessionActivityProvider.js.map +1 -0
- package/dist/{sessionListService.d.ts → sessions/sessionListService.d.ts} +3 -3
- package/dist/{sessionListService.js → sessions/sessionListService.js} +23 -6
- package/dist/sessions/sessionListService.js.map +1 -0
- package/dist/{sessionMetadataStore.d.ts → sessions/sessionMetadataStore.d.ts} +18 -0
- package/dist/{sessionMetadataStore.js → sessions/sessionMetadataStore.js} +36 -1
- package/dist/sessions/sessionMetadataStore.js.map +1 -0
- package/dist/{sessionSearch.d.ts → sessions/sessionSearch.d.ts} +1 -1
- package/dist/sessions/sessionSearch.js.map +1 -0
- package/dist/shared/hostDisplayName.d.ts +11 -0
- package/dist/shared/hostDisplayName.js +50 -0
- package/dist/shared/hostDisplayName.js.map +1 -0
- package/dist/{lineWriter.d.ts → shared/lineWriter.d.ts} +1 -1
- package/dist/{lineWriter.js → shared/lineWriter.js} +1 -1
- package/dist/shared/lineWriter.js.map +1 -0
- package/dist/{observationLog.js.map → shared/observationLog.js.map} +1 -1
- package/dist/{paths.d.ts → shared/paths.d.ts} +7 -0
- package/dist/{paths.js → shared/paths.js} +9 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/permissionMode.d.ts +10 -0
- package/dist/shared/permissionMode.js +51 -0
- package/dist/shared/permissionMode.js.map +1 -0
- package/dist/shared/skillInjection.d.ts +9 -0
- package/dist/shared/skillInjection.js +16 -0
- package/dist/shared/skillInjection.js.map +1 -0
- package/dist/shared/sleep.js.map +1 -0
- package/dist/{socketPath.d.ts → shared/socketPath.d.ts} +4 -0
- package/dist/{socketPath.js → shared/socketPath.js} +22 -0
- package/dist/shared/socketPath.js.map +1 -0
- package/dist/shared/version.js.map +1 -0
- package/package.json +13 -7
- package/dist/apnsConfigStore.js.map +0 -1
- package/dist/apnsJwtProvider.js.map +0 -1
- package/dist/approvalPushNotifier.js.map +0 -1
- package/dist/broker.js.map +0 -1
- package/dist/chatTailController.js.map +0 -1
- package/dist/claudeSessionStore.d.ts +0 -15
- package/dist/claudeSessionStore.js +0 -201
- package/dist/claudeSessionStore.js.map +0 -1
- package/dist/codexRolloutTailer.d.ts +0 -56
- package/dist/codexRolloutTailer.js +0 -303
- package/dist/codexRolloutTailer.js.map +0 -1
- package/dist/codexSessionStore.js.map +0 -1
- package/dist/codexUsage.js.map +0 -1
- package/dist/deviceTokenStore.js.map +0 -1
- package/dist/dirLister.js +0 -98
- package/dist/dirLister.js.map +0 -1
- package/dist/doctor.d.ts +0 -36
- package/dist/doctor.js +0 -162
- package/dist/doctor.js.map +0 -1
- package/dist/engine.d.ts +0 -85
- package/dist/engine.js +0 -962
- package/dist/engine.js.map +0 -1
- package/dist/hook.js.map +0 -1
- package/dist/hookSettings.js.map +0 -1
- package/dist/idleReaper.d.ts +0 -13
- package/dist/idleReaper.js +0 -30
- package/dist/idleReaper.js.map +0 -1
- package/dist/imageService.js.map +0 -1
- package/dist/kick.js.map +0 -1
- package/dist/launch.js +0 -396
- package/dist/launch.js.map +0 -1
- package/dist/legacyHomeMigration.js.map +0 -1
- package/dist/lineWriter.js.map +0 -1
- package/dist/mdnsAdvertiser.d.ts +0 -46
- package/dist/mdnsAdvertiser.js +0 -268
- package/dist/mdnsAdvertiser.js.map +0 -1
- package/dist/pairingCode.js.map +0 -1
- package/dist/paths.js.map +0 -1
- package/dist/permissionMode.d.ts +0 -8
- package/dist/permissionMode.js +0 -32
- package/dist/permissionMode.js.map +0 -1
- package/dist/planUsageFetcher.d.ts +0 -35
- package/dist/planUsageFetcher.js +0 -172
- package/dist/planUsageFetcher.js.map +0 -1
- package/dist/pushTokenCommand.js.map +0 -1
- package/dist/pushTypes.js.map +0 -1
- package/dist/sendLog.js.map +0 -1
- package/dist/sessionActivityProvider.d.ts +0 -7
- package/dist/sessionActivityProvider.js +0 -44
- package/dist/sessionActivityProvider.js.map +0 -1
- package/dist/sessionIdleTracker.d.ts +0 -19
- package/dist/sessionIdleTracker.js +0 -42
- package/dist/sessionIdleTracker.js.map +0 -1
- package/dist/sessionListService.js.map +0 -1
- package/dist/sessionMetadataStore.js.map +0 -1
- package/dist/sessionSearch.js.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/sleep.js.map +0 -1
- package/dist/socketPath.js.map +0 -1
- package/dist/subagentTailer.d.ts +0 -15
- package/dist/subagentTailer.js +0 -399
- package/dist/subagentTailer.js.map +0 -1
- package/dist/tmux.d.ts +0 -62
- package/dist/tmux.js +0 -163
- package/dist/tmux.js.map +0 -1
- package/dist/transcriptTailer.js.map +0 -1
- package/dist/usageAggregator.js.map +0 -1
- package/dist/version.js.map +0 -1
- /package/dist/{kick.d.ts → commands/kick.d.ts} +0 -0
- /package/dist/{legacyHomeMigration.d.ts → commands/legacyHomeMigration.d.ts} +0 -0
- /package/dist/{legacyHomeMigration.js → commands/legacyHomeMigration.js} +0 -0
- /package/dist/{apnsConfigStore.d.ts → push/apnsConfigStore.d.ts} +0 -0
- /package/dist/{apnsConfigStore.js → push/apnsConfigStore.js} +0 -0
- /package/dist/{apnsJwtProvider.d.ts → push/apnsJwtProvider.d.ts} +0 -0
- /package/dist/{apnsJwtProvider.js → push/apnsJwtProvider.js} +0 -0
- /package/dist/{apnsSender.d.ts → push/apnsSender.d.ts} +0 -0
- /package/dist/{apnsSender.js → push/apnsSender.js} +0 -0
- /package/dist/{approvalPushNotifier.d.ts → push/approvalPushNotifier.d.ts} +0 -0
- /package/dist/{deviceTokenStore.d.ts → push/deviceTokenStore.d.ts} +0 -0
- /package/dist/{deviceTokenStore.js → push/deviceTokenStore.js} +0 -0
- /package/dist/{pushTypes.d.ts → push/pushTypes.d.ts} +0 -0
- /package/dist/{sendLog.d.ts → push/sendLog.d.ts} +0 -0
- /package/dist/{sendLog.js → push/sendLog.js} +0 -0
- /package/dist/{usageAggregator.d.ts → services/usageAggregator.d.ts} +0 -0
- /package/dist/{usageAggregator.js → services/usageAggregator.js} +0 -0
- /package/dist/{sessionSearch.js → sessions/sessionSearch.js} +0 -0
- /package/dist/{observationLog.d.ts → shared/observationLog.d.ts} +0 -0
- /package/dist/{observationLog.js → shared/observationLog.js} +0 -0
- /package/dist/{sleep.d.ts → shared/sleep.d.ts} +0 -0
- /package/dist/{sleep.js → shared/sleep.js} +0 -0
- /package/dist/{version.d.ts → shared/version.d.ts} +0 -0
- /package/dist/{version.js → shared/version.js} +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// quicGateway.ts — QUIC ゲートウェイ(tailii-quic-gw)の解決・資格情報・launchd 常駐
|
|
2
|
+
//
|
|
3
|
+
// 設計正本: docs/quic-transport.md(モノレポルート)。
|
|
4
|
+
// ゲートウェイ本体は Rust バイナリ(quic-gw/)。この module は host-ts 側の運用面
|
|
5
|
+
// (バイナリ解決 / 資格情報の生成読込 / launchd LaunchAgent の設置)だけを担う。
|
|
6
|
+
//
|
|
7
|
+
// - 資格情報(P-256 証明書 + 32byte トークン)の生成は Rust 側
|
|
8
|
+
// `tailii-quic-gw credentials --json` に委譲する(Node に X.509 生成が無いため)。
|
|
9
|
+
// - ゲートウェイは launchd 常駐(KeepAlive)。SSH と違い、接続前に誰かが起こして
|
|
10
|
+
// おく必要があるため(Mac 再起動後の初回接続で誰も居ない問題の回避)。
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { createHash, X509Certificate } from "node:crypto";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { findCommand } from "../commands/doctor.js";
|
|
19
|
+
import { defaultInjectedPath } from "../commands/launch.js";
|
|
20
|
+
/** launchd ジョブのラベル(plist ファイル名と対)。 */
|
|
21
|
+
export const QUIC_GW_LAUNCHD_LABEL = "com.tailii.quic-gw";
|
|
22
|
+
/** ゲートウェイの既定 UDP ポート(凍結定数、Rust 側 DEFAULT_PORT と一致)。 */
|
|
23
|
+
export const QUIC_GW_DEFAULT_PORT = 46853;
|
|
24
|
+
/** 資格情報ディレクトリ(Rust 側 default_credentials_dir と対)。 */
|
|
25
|
+
export function quicCredentialsDir() {
|
|
26
|
+
return path.join(os.homedir(), ".tailii", "quic");
|
|
27
|
+
}
|
|
28
|
+
/** ゲートウェイの監査ログパス(launchd が stdout/stderr をここへ向ける)。 */
|
|
29
|
+
export function quicGatewayLogPath() {
|
|
30
|
+
return path.join(os.homedir(), ".tailii", "quic-gw.log");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* launchd が起動する gateway バイナリの設置先(`~/.tailii/bin/tailii-quic-gw`)。
|
|
34
|
+
*
|
|
35
|
+
* 重要(実機で判明した罠): launchd の LaunchAgent は `~/Documents` `~/Desktop`
|
|
36
|
+
* `~/Downloads` 等の **TCC 保護フォルダにアクセスできない**。ビルド成果物や npm 配布物が
|
|
37
|
+
* そうした場所にあると、launchd 起動時に dyld がバイナリを map できず **dyld 段階で無限
|
|
38
|
+
* ストール**する(プロセスは生存するが bind に到達せず、ログも出ない)。Terminal 起動は
|
|
39
|
+
* ユーザーの TCC 付与を継承するため成功し、差分に気付きにくい。回避のため、常駐用バイナリは
|
|
40
|
+
* 必ず非 TCC 保護の隠しディレクトリ `~/.tailii/bin` へコピーしてから launchd に渡す。
|
|
41
|
+
*/
|
|
42
|
+
export function quicGatewayInstalledBinaryPath() {
|
|
43
|
+
return path.join(os.homedir(), ".tailii", "bin", "tailii-quic-gw");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* gateway バイナリを非 TCC 保護の `~/.tailii/bin` へコピーして、その設置先パスを返す。
|
|
47
|
+
* 内容が同一なら再コピーしない(mtime/サイズで判定)。常に実行ビットを立てる。
|
|
48
|
+
*/
|
|
49
|
+
export function installQuicGatewayBinary(sourcePath) {
|
|
50
|
+
const dest = quicGatewayInstalledBinaryPath();
|
|
51
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
52
|
+
const sourceStat = fs.statSync(sourcePath);
|
|
53
|
+
let needCopy = true;
|
|
54
|
+
try {
|
|
55
|
+
const destStat = fs.statSync(dest);
|
|
56
|
+
// サイズ一致 かつ コピー先が新しければ据え置き(cargo 再ビルドは mtime が進む)。
|
|
57
|
+
needCopy = !(destStat.size === sourceStat.size && destStat.mtimeMs >= sourceStat.mtimeMs);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
needCopy = true;
|
|
61
|
+
}
|
|
62
|
+
if (needCopy) {
|
|
63
|
+
fs.copyFileSync(sourcePath, dest);
|
|
64
|
+
}
|
|
65
|
+
fs.chmodSync(dest, 0o755);
|
|
66
|
+
return dest;
|
|
67
|
+
}
|
|
68
|
+
/** LaunchAgent plist の設置先。 */
|
|
69
|
+
export function quicLaunchAgentPlistPath() {
|
|
70
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", `${QUIC_GW_LAUNCHD_LABEL}.plist`);
|
|
71
|
+
}
|
|
72
|
+
// MARK: - バイナリ解決
|
|
73
|
+
/**
|
|
74
|
+
* ゲートウェイバイナリを解決する。見つからなければ null(QUIC は任意機能なので
|
|
75
|
+
* 呼び出し側は SSH-only で継続してよい)。
|
|
76
|
+
*
|
|
77
|
+
* 解決順:
|
|
78
|
+
* 1. 環境変数 `TAILII_QUIC_GW`(明示上書き)
|
|
79
|
+
* 2. npm prebuilt(optionalDependencies `@tailii/quic-gw-darwin-{arm64,x64}`)
|
|
80
|
+
* 3. モノレポのローカル cargo ビルド(quic-gw/target/release/tailii-quic-gw)
|
|
81
|
+
* 4. PATH
|
|
82
|
+
*/
|
|
83
|
+
export function resolveQuicGatewayBinary(env = process.env) {
|
|
84
|
+
const explicit = env["TAILII_QUIC_GW"];
|
|
85
|
+
if (explicit !== undefined && explicit !== "" && fs.existsSync(explicit))
|
|
86
|
+
return explicit;
|
|
87
|
+
// 2) prebuilt npm package(配布時。未導入環境では解決失敗して次へ)。
|
|
88
|
+
const arch = os.arch() === "arm64" ? "arm64" : "x64";
|
|
89
|
+
try {
|
|
90
|
+
const require = createRequire(import.meta.url);
|
|
91
|
+
const pkgDir = path.dirname(require.resolve(`@tailii/quic-gw-darwin-${arch}/package.json`));
|
|
92
|
+
const candidate = path.join(pkgDir, "bin", "tailii-quic-gw");
|
|
93
|
+
if (fs.existsSync(candidate))
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// 未導入。次の解決先へ。
|
|
98
|
+
}
|
|
99
|
+
// 3) ローカル cargo ビルド(開発機)。dist/services/quicGateway.js → パッケージルート → quic-gw/。
|
|
100
|
+
const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
101
|
+
for (const profile of ["release", "debug"]) {
|
|
102
|
+
const candidate = path.join(pkgRoot, "quic-gw", "target", profile, "tailii-quic-gw");
|
|
103
|
+
if (fs.existsSync(candidate))
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
// 4) PATH。
|
|
107
|
+
return findCommand("tailii-quic-gw", env["PATH"] ?? "");
|
|
108
|
+
}
|
|
109
|
+
const defaultRunner = (file, args) => new Promise((resolve, reject) => {
|
|
110
|
+
execFile(file, args, { timeout: 15_000 }, (error, stdout) => {
|
|
111
|
+
if (error)
|
|
112
|
+
reject(error);
|
|
113
|
+
else
|
|
114
|
+
resolve(stdout);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* 資格情報を生成(既存なら読込・冪等)して返す。
|
|
119
|
+
* 実体は `tailii-quic-gw credentials --json --dir <dir>`。
|
|
120
|
+
*/
|
|
121
|
+
export async function ensureQuicCredentials(gatewayPath, dir = quicCredentialsDir(), runner = defaultRunner) {
|
|
122
|
+
const stdout = await runner(gatewayPath, ["credentials", "--json", "--dir", dir]);
|
|
123
|
+
const parsed = JSON.parse(stdout.trim());
|
|
124
|
+
if (typeof parsed !== "object" || parsed === null ||
|
|
125
|
+
typeof parsed.spkiPin !== "string" ||
|
|
126
|
+
typeof parsed.token !== "string" ||
|
|
127
|
+
typeof parsed.port !== "number") {
|
|
128
|
+
throw new Error(`tailii-quic-gw credentials --json の出力が不正です: ${stdout.trim()}`);
|
|
129
|
+
}
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
132
|
+
// MARK: - launchd LaunchAgent
|
|
133
|
+
/** XML テキストノードのエスケープ(plist 値に埋める文字列用)。 */
|
|
134
|
+
function xmlEscape(value) {
|
|
135
|
+
return value
|
|
136
|
+
.replaceAll("&", "&")
|
|
137
|
+
.replaceAll("<", "<")
|
|
138
|
+
.replaceAll(">", ">");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* LaunchAgent plist の内容(KeepAlive 常駐・監査ログは stdout/stderr リダイレクト)。
|
|
142
|
+
*
|
|
143
|
+
* 重要(実機で判明した罠): macOS の GUI LaunchAgent は **HOME を継承しない**。
|
|
144
|
+
* ゲートウェイは `~/.tailii/quic` の展開・`default_injected_path` の両方で HOME を読むため、
|
|
145
|
+
* HOME 空だと資格情報ディレクトリが `/.tailii/quic` に化けて `create_dir_all` が失敗し、
|
|
146
|
+
* serve が即終了 → KeepAlive 再起動ループに陥る。対策として:
|
|
147
|
+
* 1. `EnvironmentVariables` に HOME を明示(gateway 本体 + exec 子プロセス双方に効く)。
|
|
148
|
+
* 2. `--dir` / `--path` を **絶対パスで明示的に渡し**、HOME 展開へ依存しない(防御多重化)。
|
|
149
|
+
*/
|
|
150
|
+
export function quicLaunchAgentPlist(config) {
|
|
151
|
+
const args = [
|
|
152
|
+
config.gatewayPath,
|
|
153
|
+
"serve",
|
|
154
|
+
"--dir",
|
|
155
|
+
config.credentialsDir,
|
|
156
|
+
"--path",
|
|
157
|
+
config.injectedPath,
|
|
158
|
+
];
|
|
159
|
+
const programArguments = args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
160
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
161
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
162
|
+
<plist version="1.0">
|
|
163
|
+
<dict>
|
|
164
|
+
<key>Label</key>
|
|
165
|
+
<string>${QUIC_GW_LAUNCHD_LABEL}</string>
|
|
166
|
+
<key>ProgramArguments</key>
|
|
167
|
+
<array>
|
|
168
|
+
${programArguments}
|
|
169
|
+
</array>
|
|
170
|
+
<key>EnvironmentVariables</key>
|
|
171
|
+
<dict>
|
|
172
|
+
<key>HOME</key>
|
|
173
|
+
<string>${xmlEscape(config.home)}</string>
|
|
174
|
+
<key>PATH</key>
|
|
175
|
+
<string>${xmlEscape(config.injectedPath)}</string>
|
|
176
|
+
</dict>
|
|
177
|
+
<key>RunAtLoad</key>
|
|
178
|
+
<true/>
|
|
179
|
+
<key>KeepAlive</key>
|
|
180
|
+
<true/>
|
|
181
|
+
<key>StandardOutPath</key>
|
|
182
|
+
<string>${xmlEscape(config.logPath)}</string>
|
|
183
|
+
<key>StandardErrorPath</key>
|
|
184
|
+
<string>${xmlEscape(config.logPath)}</string>
|
|
185
|
+
</dict>
|
|
186
|
+
</plist>
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
const defaultLaunchctl = (args) => new Promise((resolve, reject) => {
|
|
190
|
+
execFile("launchctl", args, { timeout: 15_000 }, (error) => {
|
|
191
|
+
if (error)
|
|
192
|
+
reject(error);
|
|
193
|
+
else
|
|
194
|
+
resolve();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
/**
|
|
198
|
+
* LaunchAgent を設置して(再)起動する(冪等)。
|
|
199
|
+
* 既存ジョブは bootout してから bootstrap し、plist 更新を確実に反映する。
|
|
200
|
+
*
|
|
201
|
+
* gateway バイナリは非 TCC 保護の `~/.tailii/bin` へコピーしてから launchd に渡す
|
|
202
|
+
* (`installQuicGatewayBinary` 参照。TCC 保護フォルダ由来の dyld ストール回避)。
|
|
203
|
+
*/
|
|
204
|
+
export async function installQuicLaunchAgent(options) {
|
|
205
|
+
const plistPath = options.plistPath ?? quicLaunchAgentPlistPath();
|
|
206
|
+
const logPath = options.logPath ?? quicGatewayLogPath();
|
|
207
|
+
const uid = options.uid ?? process.getuid?.() ?? 501;
|
|
208
|
+
const launchctl = options.launchctl ?? defaultLaunchctl;
|
|
209
|
+
const installBinary = options.installBinary ?? installQuicGatewayBinary;
|
|
210
|
+
// TCC 保護フォルダ(~/Documents 等)から launchd 起動すると dyld がストールするため、
|
|
211
|
+
// 常駐用バイナリは非保護の隠しディレクトリへコピーしてそこを起動する。
|
|
212
|
+
const launchBinary = installBinary(options.gatewayPath);
|
|
213
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
214
|
+
fs.writeFileSync(plistPath, quicLaunchAgentPlist({
|
|
215
|
+
gatewayPath: launchBinary,
|
|
216
|
+
logPath,
|
|
217
|
+
credentialsDir: options.credentialsDir ?? quicCredentialsDir(),
|
|
218
|
+
injectedPath: options.injectedPath ?? defaultInjectedPath(),
|
|
219
|
+
home: options.home ?? os.homedir(),
|
|
220
|
+
}), { mode: 0o644 });
|
|
221
|
+
// 既存ジョブは落としてから載せ直す(未登録の bootout 失敗は無視)。
|
|
222
|
+
try {
|
|
223
|
+
await launchctl(["bootout", `gui/${uid}/${QUIC_GW_LAUNCHD_LABEL}`]);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// 未登録なら失敗するのが正常。
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
await launchctl(["bootstrap", `gui/${uid}`, plistPath]);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// 旧 macOS / 非 GUI セッション向けフォールバック。
|
|
233
|
+
await launchctl(["load", "-w", plistPath]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** launchd ジョブが常駐しているか(doctor / quic-info 用・副作用なし)。 */
|
|
237
|
+
export async function isQuicGatewayLoaded(uid = process.getuid?.() ?? 501, launchctl = defaultLaunchctl) {
|
|
238
|
+
try {
|
|
239
|
+
await launchctl(["print", `gui/${uid}/${QUIC_GW_LAUNCHD_LABEL}`]);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 証明書 PEM から SPKI SHA-256 ピン(base64)を計算する。
|
|
248
|
+
* Rust 側 `Credentials::spki_pin()`(rcgen public_key_der のハッシュ)と同一値になる
|
|
249
|
+
* (X.509 の SubjectPublicKeyInfo DER は同一バイト列のため)。
|
|
250
|
+
*/
|
|
251
|
+
export function computeSpkiPinFromCertPem(certPem) {
|
|
252
|
+
const cert = new X509Certificate(certPem);
|
|
253
|
+
const spkiDer = cert.publicKey.export({ type: "spki", format: "der" });
|
|
254
|
+
return createHash("sha256").update(spkiDer).digest("base64");
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* ディスク上の資格情報(cert.pem / token)から quic-info の中身を読む。
|
|
258
|
+
* ゲートウェイバイナリには依存しない(生成は setup / Rust 側の責務。ここは読むだけ)。
|
|
259
|
+
*/
|
|
260
|
+
export function readQuicCredentialsFromDisk(dir = quicCredentialsDir()) {
|
|
261
|
+
try {
|
|
262
|
+
const certPem = fs.readFileSync(path.join(dir, "cert.pem"), "utf8");
|
|
263
|
+
const token = fs.readFileSync(path.join(dir, "token"), "utf8").trim();
|
|
264
|
+
if (token === "")
|
|
265
|
+
return null;
|
|
266
|
+
return { pin: computeSpkiPinFromCertPem(certPem), token };
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** quic-info の本体(テスト注入可能な純ロジック)。 */
|
|
273
|
+
export async function collectQuicInfo(options) {
|
|
274
|
+
const creds = readQuicCredentialsFromDisk(options?.dir ?? quicCredentialsDir());
|
|
275
|
+
if (creds === null) {
|
|
276
|
+
return { available: false, reason: "credentials-missing" };
|
|
277
|
+
}
|
|
278
|
+
const loaded = await (options?.isLoaded ?? (() => isQuicGatewayLoaded()))();
|
|
279
|
+
if (!loaded) {
|
|
280
|
+
// 常駐していないのに資格情報だけ配ると、クライアントが再接続のたびに
|
|
281
|
+
// 死んだゲートウェイへ 1.5s の接続試行税を払う。配らない方が安全側。
|
|
282
|
+
return { available: false, reason: "gateway-not-running" };
|
|
283
|
+
}
|
|
284
|
+
return { available: true, port: QUIC_GW_DEFAULT_PORT, pin: creds.pin, token: creds.token };
|
|
285
|
+
}
|
|
286
|
+
/** `tailii-host quic-info` エントリポイント。stdout に JSON 1 行を出す(常に exit 0)。 */
|
|
287
|
+
export async function runQuicInfoCommand(_argv) {
|
|
288
|
+
const info = await collectQuicInfo();
|
|
289
|
+
process.stdout.write(JSON.stringify(info) + "\n");
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=quicGateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quicGateway.js","sourceRoot":"","sources":["../../src/services/quicGateway.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ServeProcessInfo } from "../protocol.js";
|
|
2
|
+
/** `*:5173` / `127.0.0.1:5173` / `[::1]:5173` からポート番号を取り出す。 */
|
|
3
|
+
export declare function parseListenPort(name: string): number | null;
|
|
4
|
+
/**
|
|
5
|
+
* `lsof -Fpcn` の機械可読出力を (pid, port) の組へパースする。
|
|
6
|
+
* 出力はプロセスブロック(p/c 行)に n 行(bind 先)が続く形。
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseLsofListenOutput(output: string): {
|
|
9
|
+
pid: number;
|
|
10
|
+
command: string;
|
|
11
|
+
port: number;
|
|
12
|
+
}[];
|
|
13
|
+
/** `lsof -a -d cwd -Fpn -p <pids>` の出力を pid → cwd へパースする。 */
|
|
14
|
+
export declare function parseLsofCwdOutput(output: string): Map<number, string>;
|
|
15
|
+
/**
|
|
16
|
+
* HTML 断片から `<title>` の中身を取り出す。空・見つからない場合は null。
|
|
17
|
+
* 表示用に空白を畳み、基本的な文字参照を復号して TITLE_LIMIT で切り詰める。
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseHtmlTitle(html: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* 自ユーザーが loopback/wildcard で LISTEN している TCP サーバーを列挙する。
|
|
22
|
+
* `excludePids` は engine 自身など一覧に出さない pid。
|
|
23
|
+
* `withTitles` で各ポートへ HTTP GET し HTML の `<title>` を付与する
|
|
24
|
+
* (一覧表示用。停止前照合など内部利用では省く)。
|
|
25
|
+
*/
|
|
26
|
+
export interface ServeProcessListResult {
|
|
27
|
+
/** false は lsof 未導入・timeout 等で一覧の完全性を保証できないことを表す。 */
|
|
28
|
+
ok: boolean;
|
|
29
|
+
servers: ServeProcessInfo[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 一覧と検出成否を返す。reaper は `ok=false` を「サーバーなし」とみなさず回収を延期する。
|
|
33
|
+
*/
|
|
34
|
+
export declare function listServeProcessesWithStatus(options?: {
|
|
35
|
+
excludePids?: number[];
|
|
36
|
+
withTitles?: boolean;
|
|
37
|
+
}): Promise<ServeProcessListResult>;
|
|
38
|
+
/** 従来の一覧 API。UI/停止処理は失敗時も取得できた部分結果を表示する。 */
|
|
39
|
+
export declare function listServeProcesses(options?: {
|
|
40
|
+
excludePids?: number[];
|
|
41
|
+
withTitles?: boolean;
|
|
42
|
+
}): Promise<ServeProcessInfo[]>;
|
|
43
|
+
/**
|
|
44
|
+
* pid のサーバーを停止する。pid 再利用の誤爆を防ぐため、その pid が今も
|
|
45
|
+
* 指定 port で LISTEN していることを確認してから SIGTERM → SIGKILL する。
|
|
46
|
+
*/
|
|
47
|
+
export declare function stopServeProcess(pid: number, port: number): Promise<{
|
|
48
|
+
ok: boolean;
|
|
49
|
+
error: string | null;
|
|
50
|
+
}>;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// serveService.ts
|
|
2
|
+
// tailii (TS host) — Mac 上で LISTEN 中の開発サーバー一覧と停止(serve-list)
|
|
3
|
+
//
|
|
4
|
+
// iOS の「Webサーバー」一覧は、この Mac で自ユーザーが LISTEN している TCP ポートを
|
|
5
|
+
// lsof で列挙して表示する。dev サーバー(vite / next / python -m http.server 等)は
|
|
6
|
+
// プロジェクト dir を cwd に持つため、cwd を返して iOS 側で会話 workdir との
|
|
7
|
+
// グルーピングに使う。ノイズ削減のため以下は host 側で除外する:
|
|
8
|
+
// - engine 自身(previewServer の loopback 静的サーバーを含む)
|
|
9
|
+
// - cwd が "/" のプロセス(launchd 起動の常駐アプリ・デーモン類。dev サーバーは
|
|
10
|
+
// 必ず実プロジェクトの cwd を持つ)
|
|
11
|
+
// - loopback/wildcard 以外へ bind しているソケット(LAN 固定 bind は対象外)
|
|
12
|
+
//
|
|
13
|
+
// 停止は pid 再利用による誤 kill を防ぐため「その pid が今もそのポートで LISTEN
|
|
14
|
+
// している」ことを確認してから SIGTERM → 猶予後 SIGKILL の順で行う。
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
const LSOF_TIMEOUT_MS = 8_000;
|
|
17
|
+
const EXEC_MAX_BUFFER = 4 * 1024 * 1024;
|
|
18
|
+
/** SIGTERM 後にプロセス終了を待つ最大時間。 */
|
|
19
|
+
const TERM_GRACE_MS = 1_500;
|
|
20
|
+
/** SIGKILL 後にプロセス終了を待つ最大時間。 */
|
|
21
|
+
const KILL_GRACE_MS = 700;
|
|
22
|
+
const POLL_INTERVAL_MS = 100;
|
|
23
|
+
const COMMAND_LINE_LIMIT = 160;
|
|
24
|
+
/** title 取得の HTTP GET 全体タイムアウト。非 HTTP リスナーはここで切れる。 */
|
|
25
|
+
const TITLE_FETCH_TIMEOUT_MS = 1_200;
|
|
26
|
+
/** title 探索で読む HTML 先頭バイト数の上限。 */
|
|
27
|
+
const TITLE_BODY_LIMIT = 64 * 1024;
|
|
28
|
+
const TITLE_LIMIT = 80;
|
|
29
|
+
function run(command, args) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
execFile(command, args, { timeout: LSOF_TIMEOUT_MS, maxBuffer: EXEC_MAX_BUFFER, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
32
|
+
const typed = error;
|
|
33
|
+
// lsof は「該当なし」でも stderr なしの exit 1 を返す。これは信頼できる空結果。
|
|
34
|
+
// ENOENT・timeout・signal 終了は部分 stdout があっても不完全なので失敗扱いにする。
|
|
35
|
+
const noMatches = typed !== null && typed.code === 1 && String(stdout).length === 0 &&
|
|
36
|
+
String(stderr).length === 0 && typed.killed !== true && typed.signal == null;
|
|
37
|
+
resolve({
|
|
38
|
+
ok: error === null || noMatches,
|
|
39
|
+
stdout: String(stdout),
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function sleep(ms) {
|
|
45
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
46
|
+
}
|
|
47
|
+
/** `*:5173` / `127.0.0.1:5173` / `[::1]:5173` からポート番号を取り出す。 */
|
|
48
|
+
export function parseListenPort(name) {
|
|
49
|
+
const colon = name.lastIndexOf(":");
|
|
50
|
+
if (colon < 0)
|
|
51
|
+
return null;
|
|
52
|
+
const port = Number(name.slice(colon + 1));
|
|
53
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
54
|
+
return null;
|
|
55
|
+
const host = name.slice(0, colon);
|
|
56
|
+
// loopback / wildcard のみ対象(明示的な LAN bind は開発サーバー一覧の対象外)。
|
|
57
|
+
const allowed = new Set(["*", "127.0.0.1", "localhost", "[::1]", "[::]", "0.0.0.0"]);
|
|
58
|
+
return allowed.has(host) ? port : null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `lsof -Fpcn` の機械可読出力を (pid, port) の組へパースする。
|
|
62
|
+
* 出力はプロセスブロック(p/c 行)に n 行(bind 先)が続く形。
|
|
63
|
+
*/
|
|
64
|
+
export function parseLsofListenOutput(output) {
|
|
65
|
+
const results = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
let pid = null;
|
|
68
|
+
let command = "";
|
|
69
|
+
for (const line of output.split("\n")) {
|
|
70
|
+
if (line.startsWith("p")) {
|
|
71
|
+
pid = Number(line.slice(1));
|
|
72
|
+
command = "";
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (line.startsWith("c")) {
|
|
76
|
+
command = line.slice(1);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (line.startsWith("n") && pid !== null) {
|
|
80
|
+
const port = parseListenPort(line.slice(1));
|
|
81
|
+
if (port === null)
|
|
82
|
+
continue;
|
|
83
|
+
const key = `${pid}:${port}`;
|
|
84
|
+
if (seen.has(key))
|
|
85
|
+
continue;
|
|
86
|
+
seen.add(key);
|
|
87
|
+
results.push({ pid, command, port });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
/** `lsof -a -d cwd -Fpn -p <pids>` の出力を pid → cwd へパースする。 */
|
|
93
|
+
export function parseLsofCwdOutput(output) {
|
|
94
|
+
const cwds = new Map();
|
|
95
|
+
let pid = null;
|
|
96
|
+
for (const line of output.split("\n")) {
|
|
97
|
+
if (line.startsWith("p")) {
|
|
98
|
+
pid = Number(line.slice(1));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (line.startsWith("n") && pid !== null) {
|
|
102
|
+
cwds.set(pid, line.slice(1));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return cwds;
|
|
106
|
+
}
|
|
107
|
+
async function fetchCwds(pids) {
|
|
108
|
+
if (pids.length === 0)
|
|
109
|
+
return { ok: true, cwds: new Map() };
|
|
110
|
+
const result = await run("lsof", ["-a", "-d", "cwd", "-Fpn", "-p", pids.join(",")]);
|
|
111
|
+
return { ok: result.ok, cwds: parseLsofCwdOutput(result.stdout) };
|
|
112
|
+
}
|
|
113
|
+
async function fetchCommandLines(pids) {
|
|
114
|
+
if (pids.length === 0)
|
|
115
|
+
return new Map();
|
|
116
|
+
const result = await run("ps", ["-o", "pid=,command=", "-p", pids.join(",")]);
|
|
117
|
+
const lines = new Map();
|
|
118
|
+
for (const row of result.stdout.split("\n")) {
|
|
119
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(row);
|
|
120
|
+
if (match === null)
|
|
121
|
+
continue;
|
|
122
|
+
const commandLine = match[2].trim();
|
|
123
|
+
lines.set(Number(match[1]), commandLine.length > COMMAND_LINE_LIMIT
|
|
124
|
+
? `${commandLine.slice(0, COMMAND_LINE_LIMIT)}…`
|
|
125
|
+
: commandLine);
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* HTML 断片から `<title>` の中身を取り出す。空・見つからない場合は null。
|
|
131
|
+
* 表示用に空白を畳み、基本的な文字参照を復号して TITLE_LIMIT で切り詰める。
|
|
132
|
+
*/
|
|
133
|
+
export function parseHtmlTitle(html) {
|
|
134
|
+
const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
135
|
+
if (match === null)
|
|
136
|
+
return null;
|
|
137
|
+
const title = match[1]
|
|
138
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
139
|
+
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))
|
|
140
|
+
.replace(/</g, "<")
|
|
141
|
+
.replace(/>/g, ">")
|
|
142
|
+
.replace(/"/g, '"')
|
|
143
|
+
.replace(/'/g, "'")
|
|
144
|
+
.replace(/&/g, "&")
|
|
145
|
+
.replace(/\s+/g, " ")
|
|
146
|
+
.trim();
|
|
147
|
+
if (title.length === 0)
|
|
148
|
+
return null;
|
|
149
|
+
// サロゲートペア(絵文字等)を割らないようコードポイント単位で切り詰める。
|
|
150
|
+
const points = [...title];
|
|
151
|
+
return points.length > TITLE_LIMIT ? `${points.slice(0, TITLE_LIMIT).join("")}…` : title;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* loopback の HTTP GET でページ title を 1 ポートぶん取得する。失敗は null。
|
|
155
|
+
* `[::1]` のみで LISTEN するサーバーにも当たるよう IPv4/IPv6 両 loopback を並行試行する。
|
|
156
|
+
*/
|
|
157
|
+
async function probeTitle(port) {
|
|
158
|
+
const results = await Promise.all([
|
|
159
|
+
probeTitleAt(`http://127.0.0.1:${port}/`),
|
|
160
|
+
probeTitleAt(`http://[::1]:${port}/`),
|
|
161
|
+
]);
|
|
162
|
+
return results.find((title) => title !== null) ?? null;
|
|
163
|
+
}
|
|
164
|
+
async function probeTitleAt(url) {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timer = setTimeout(() => controller.abort(), TITLE_FETCH_TIMEOUT_MS);
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetch(url, {
|
|
169
|
+
signal: controller.signal,
|
|
170
|
+
headers: { accept: "text/html" },
|
|
171
|
+
});
|
|
172
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
173
|
+
// content-type 未設定の簡易サーバーは HTML の可能性を残して読む。
|
|
174
|
+
if (contentType !== "" && !contentType.toLowerCase().includes("html")) {
|
|
175
|
+
await response.body?.cancel().catch(() => { });
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (response.body === null)
|
|
179
|
+
return null;
|
|
180
|
+
const reader = response.body.getReader();
|
|
181
|
+
const decoder = new TextDecoder();
|
|
182
|
+
let html = "";
|
|
183
|
+
while (html.length < TITLE_BODY_LIMIT) {
|
|
184
|
+
const { done, value } = await reader.read();
|
|
185
|
+
if (done)
|
|
186
|
+
break;
|
|
187
|
+
html += decoder.decode(value, { stream: true });
|
|
188
|
+
if (/<\/title>/i.test(html))
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
await reader.cancel().catch(() => { });
|
|
192
|
+
return parseHtmlTitle(html);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** 各ポートの HTML title を並列取得する(全体は TITLE_FETCH_TIMEOUT_MS で頭打ち)。 */
|
|
202
|
+
async function fetchTitles(ports) {
|
|
203
|
+
const titles = new Map();
|
|
204
|
+
await Promise.all(ports.map(async (port) => {
|
|
205
|
+
const title = await probeTitle(port);
|
|
206
|
+
if (title !== null)
|
|
207
|
+
titles.set(port, title);
|
|
208
|
+
}));
|
|
209
|
+
return titles;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 一覧と検出成否を返す。reaper は `ok=false` を「サーバーなし」とみなさず回収を延期する。
|
|
213
|
+
*/
|
|
214
|
+
export async function listServeProcessesWithStatus(options = {}) {
|
|
215
|
+
const excluded = new Set(options.excludePids ?? []);
|
|
216
|
+
const listen = await run("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-Fpcn"]);
|
|
217
|
+
const entries = parseLsofListenOutput(listen.stdout)
|
|
218
|
+
.filter((entry) => !excluded.has(entry.pid));
|
|
219
|
+
const pids = [...new Set(entries.map((entry) => entry.pid))];
|
|
220
|
+
const [cwdResult, commandLines] = await Promise.all([fetchCwds(pids), fetchCommandLines(pids)]);
|
|
221
|
+
const servers = entries
|
|
222
|
+
.map((entry) => {
|
|
223
|
+
const cwd = cwdResult.cwds.get(entry.pid);
|
|
224
|
+
return {
|
|
225
|
+
pid: entry.pid,
|
|
226
|
+
port: entry.port,
|
|
227
|
+
command: entry.command,
|
|
228
|
+
...(commandLines.has(entry.pid) ? { commandLine: commandLines.get(entry.pid) } : {}),
|
|
229
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
230
|
+
};
|
|
231
|
+
})
|
|
232
|
+
// cwd "/" は launchd 起動アプリ・デーモン(dev サーバーは実 cwd を持つ)。
|
|
233
|
+
// cwd 不明(取得失敗)は残す — 情報不足を理由に隠さない。
|
|
234
|
+
.filter((entry) => entry.cwd !== "/")
|
|
235
|
+
.sort((lhs, rhs) => lhs.port - rhs.port);
|
|
236
|
+
if (options.withTitles === true && servers.length > 0) {
|
|
237
|
+
const titles = await fetchTitles([...new Set(servers.map((server) => server.port))]);
|
|
238
|
+
for (const server of servers) {
|
|
239
|
+
const title = titles.get(server.port);
|
|
240
|
+
if (title !== undefined)
|
|
241
|
+
server.title = title;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { ok: listen.ok && cwdResult.ok, servers };
|
|
245
|
+
}
|
|
246
|
+
/** 従来の一覧 API。UI/停止処理は失敗時も取得できた部分結果を表示する。 */
|
|
247
|
+
export async function listServeProcesses(options = {}) {
|
|
248
|
+
return (await listServeProcessesWithStatus(options)).servers;
|
|
249
|
+
}
|
|
250
|
+
function isAlive(pid) {
|
|
251
|
+
try {
|
|
252
|
+
process.kill(pid, 0);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function waitForExit(pid, graceMs) {
|
|
260
|
+
const deadline = Date.now() + graceMs;
|
|
261
|
+
while (Date.now() < deadline) {
|
|
262
|
+
if (!isAlive(pid))
|
|
263
|
+
return true;
|
|
264
|
+
await sleep(POLL_INTERVAL_MS);
|
|
265
|
+
}
|
|
266
|
+
return !isAlive(pid);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* pid のサーバーを停止する。pid 再利用の誤爆を防ぐため、その pid が今も
|
|
270
|
+
* 指定 port で LISTEN していることを確認してから SIGTERM → SIGKILL する。
|
|
271
|
+
*/
|
|
272
|
+
export async function stopServeProcess(pid, port) {
|
|
273
|
+
if (!Number.isInteger(pid) || pid <= 1) {
|
|
274
|
+
return { ok: false, error: `不正な pid: ${pid}` };
|
|
275
|
+
}
|
|
276
|
+
if (pid === process.pid) {
|
|
277
|
+
return { ok: false, error: "host 自身は停止できません" };
|
|
278
|
+
}
|
|
279
|
+
const current = await listServeProcesses();
|
|
280
|
+
const target = current.find((entry) => entry.pid === pid && entry.port === port);
|
|
281
|
+
if (target === undefined) {
|
|
282
|
+
return { ok: false, error: "対象のサーバーが見つかりません(すでに停止済み?)" };
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
process.kill(pid, "SIGTERM");
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
return { ok: false, error: `停止に失敗しました: ${String(error)}` };
|
|
289
|
+
}
|
|
290
|
+
if (await waitForExit(pid, TERM_GRACE_MS))
|
|
291
|
+
return { ok: true, error: null };
|
|
292
|
+
try {
|
|
293
|
+
process.kill(pid, "SIGKILL");
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// SIGTERM 後に自然終了したレース。生存確認へ進む。
|
|
297
|
+
}
|
|
298
|
+
if (await waitForExit(pid, KILL_GRACE_MS))
|
|
299
|
+
return { ok: true, error: null };
|
|
300
|
+
return { ok: false, error: "プロセスが終了しませんでした" };
|
|
301
|
+
}
|
|
302
|
+
//# sourceMappingURL=serveService.js.map
|