feat: add daemon mode with IPC, service installation, and CLI subcomm…#93
Open
Niladri2003 wants to merge 8 commits into
Open
feat: add daemon mode with IPC, service installation, and CLI subcomm…#93Niladri2003 wants to merge 8 commits into
Niladri2003 wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new background “daemon mode” to the Pinggy CLI, allowing tunnels to be started/stopped/queried from a long-running local daemon via a localhost IPC server, with optional OS service installation.
Changes:
- Introduces daemon lifecycle + IPC HTTP server/client (
src/daemon/*) and apinggy daemon/pinggy dCLI subcommand router/handlers. - Adds daemon metadata/log paths under the Pinggy config directory and an early
_daemon-childexecution path inmain. - Adds a cross-platform service installer (systemd user unit / launchd agent / Windows Task Scheduler) and updates CLI help/options accordingly.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/configDir.ts | Adds daemon info/log path helpers and ensures base config dir exists for daemon artifacts. |
| src/main.ts | Parses args earlier, configures logging, and adds an early _daemon-child branch to run the daemon child process. |
| src/daemon/serviceInstaller.ts | Adds platform-specific service installation/uninstallation logic for the daemon. |
| src/daemon/ipcServer.ts | Implements a localhost-only HTTP IPC server exposing ping/list/start/stop/shutdown endpoints. |
| src/daemon/ipcClient.ts | Implements a minimal HTTP client used by the foreground CLI to talk to the daemon. |
| src/daemon/daemonManager.ts | Implements daemon start/stop/status logic via spawning and daemon.json polling. |
| src/daemon/daemonChild.ts | Implements the daemon child entrypoint: starts IPC server, writes daemon.json, autostarts tunnels, and handles shutdown. |
| src/cli/subcommands.ts | Extends subcommand routing to include daemon/d. |
| src/cli/options.ts | Adds hidden internal _daemon-child option to support child-process mode. |
| src/cli/help.ts | Updates help text to document daemon commands (but currently drops pinggy start docs). |
| src/cli/daemonCommands.ts | Adds daemon subcommand verbs: start/stop/status/ps/tunnel-stop/service-install/service-uninstall. |
Comments suppressed due to low confidence (1)
src/cli/subcommands.ts:41
isSubcommand()only checksrawArgs[0], sopinggy --loglevel debug daemon start(or any global flags before the subcommand) won’t route into subcommand mode even thoughparseCliArgs()would correctly identify the first positional. Consider switching subcommand detection to use parsedpositionals[0](or strip leading options) so subcommands work when global flags precede them.
const SUBCOMMANDS = new Set(["config", "start", "daemon", "d"]);
/**
* Check if the raw args start with a known subcommand.
*/
export function isSubcommand(rawArgs: string[]): boolean {
return rawArgs.length > 0 && SUBCOMMANDS.has(rawArgs[0]);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+140
to
+149
| export async function stopDaemon(): Promise<boolean> { | ||
| const info = getDaemonInfo(); | ||
| if (!info) return false; | ||
|
|
||
| try { | ||
| // Try HTTP shutdown first (works on all platforms including Windows) | ||
| const { IPCClient } = await import("./ipcClient.js"); | ||
| const client = new IPCClient(info.port); | ||
| await client.shutdown(); | ||
| return true; |
Comment on lines
+87
to
+112
| export async function startDaemon(): Promise<DaemonInfo> { | ||
| // Check if already running | ||
| const existing = getDaemonInfo(); | ||
| if (existing) { | ||
| return existing; | ||
| } | ||
|
|
||
| const { command, args } = getDaemonSpawnArgs(); | ||
| logger.info("Spawning daemon child", { command, args }); | ||
|
|
||
| const child = spawn(command, args, { | ||
| detached: true, | ||
| stdio: "ignore", | ||
| env: { ...process.env }, | ||
| }); | ||
|
|
||
| child.unref(); | ||
|
|
||
| // Wait for daemon.json to appear (child writes it after IPC server binds) | ||
| const info = await pollForDaemonInfo(DAEMON_SPAWN_TIMEOUT_MS); | ||
| if (!info) { | ||
| throw new Error("Daemon failed to start within timeout. Check daemon.log for details."); | ||
| } | ||
|
|
||
| return info; | ||
| } |
Comment on lines
+155
to
+159
| function installWindows(): void { | ||
| const binaryPath = resolveBinaryPath(); | ||
| const cmd = `schtasks /Create /TN "${WINDOWS_TASK_NAME}" /TR "${binaryPath} -D" /SC ONLOGON /RL LIMITED /F`; | ||
| execSync(cmd, { stdio: "inherit" }); | ||
| console.log(`Scheduled task "${WINDOWS_TASK_NAME}" created (runs at login).`); |
Comment on lines
+42
to
+62
| this.routes.set("POST /tunnels/start", async (body) => { | ||
| const { name } = JSON.parse(body); | ||
| if (!name) return { error: "Missing 'name' field" }; | ||
| // Import at call time to avoid circular deps | ||
| const { findConfig } = await import("../cli/configStore.js"); | ||
| const saved = findConfig(name); | ||
| if (!saved) return { error: `No config found matching "${name}"` }; | ||
|
|
||
| const config = { | ||
| ...saved.tunnelConfig, | ||
| configId: saved.configId, | ||
| name: saved.name, | ||
| } as TunnelConfigV1; | ||
| return await this.ops.handleStartV2(config); | ||
| }); | ||
|
|
||
| this.routes.set("POST /tunnels/stop", async (body) => { | ||
| const { tunnelid } = JSON.parse(body); | ||
| if (!tunnelid) return { error: "Missing 'tunnelid' field" }; | ||
| return await this.ops.handleStop(tunnelid); | ||
| }); |
Comment on lines
+1
to
+10
| /** | ||
| * Daemon child process entry point. | ||
| * Called when the CLI is invoked with --_daemon-child. | ||
| * | ||
| * Responsibilities: | ||
| * 1. Redirect stdout/stderr to daemon.log | ||
| * 2. Start the IPC HTTP server | ||
| * 3. Write daemon.json (port + pid) atomically | ||
| * 4. Start all auto-start tunnels | ||
| * 5. Handle graceful shutdown (cleanup daemon.json) |
Comment on lines
+38
to
+52
| function generateSystemdUnit(binaryPath: string): string { | ||
| return `[Unit] | ||
| Description=Pinggy Tunnel Daemon | ||
| After=network-online.target | ||
| Wants=network-online.target | ||
|
|
||
| [Service] | ||
| Type=forking | ||
| ExecStart=${binaryPath} -D | ||
| Restart=on-failure | ||
| RestartSec=5 | ||
|
|
||
| [Install] | ||
| WantedBy=default.target | ||
| `; |
Comment on lines
+93
to
+99
| function generateLaunchdPlist(binaryPath: string): string { | ||
| // Split binary path for cases like "node /path/to/script" | ||
| const parts = binaryPath.split(" "); | ||
| const programArgs = parts.map((p) => ` <string>${p}</string>`).join("\n"); | ||
|
|
||
| return `<?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" |
…ands Introduce background daemon support via `pinggy daemon` (or `pinggy d`) subcommand with start/stop/status/ps/tunnel-stop/service-install verbs. Includes IPC server/client for tunnel management, system service installer (systemd/launchd/schtasks), and daemon child process handling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nd service installer
…ing and help messages and removed unused codes
- daemonManager spawns via ELECTRON_RUN_AS_NODE when hosted by Electron,
resolving the cli-js entry through createRequire so argv[1] no longer
needs to point at us.
- IPCClient/TunnelClient take an origin ("app"|"cli"|"remote") that
rides on X-Pinggy-Origin. Daemon stores it on ManagedTunnel, persists
it in daemon-state.json, and uses it in the log filename as
<origin>__<name>__<tunnelId>.log so app vs cli tunnels are
distinguishable on disk and in /logs/paths responses.
- TunnelClient gains the TunnelOperations-shaped wrappers needed for
the desktop app to use it as a drop-in for the in-process SDK.
- Tunnel-logging toggle: setTunnelLoggingEnabled / isTunnelLoggingEnabled
in tunnelLogger.ts, GET/POST /config/tunnel-logging routes, exposed
via TunnelClient. Disabling detaches live transports immediately.
- getActiveTunnelIds() on TunnelManager so /logs/paths reports
running=true only for tunnels still alive in memory (not stopped or
fatally errored).
- New restartCommand, logCommand, logsCommand subcommands plus help
and options updates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…ands
Introduce background daemon support via
pinggy daemon(orpinggy d) subcommand with start/stop/status/ps/tunnel-stop/service-install verbs. Includes IPC server/client for tunnel management, system service installer (systemd/launchd/schtasks), and daemon child process handling.