Skip to content

feat: add daemon mode with IPC, service installation, and CLI subcomm…#93

Open
Niladri2003 wants to merge 8 commits into
mainfrom
feat/daemon
Open

feat: add daemon mode with IPC, service installation, and CLI subcomm…#93
Niladri2003 wants to merge 8 commits into
mainfrom
feat/daemon

Conversation

@Niladri2003
Copy link
Copy Markdown
Contributor

…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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a pinggy daemon / pinggy d CLI subcommand router/handlers.
  • Adds daemon metadata/log paths under the Pinggy config directory and an early _daemon-child execution path in main.
  • 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 checks rawArgs[0], so pinggy --loglevel debug daemon start (or any global flags before the subcommand) won’t route into subcommand mode even though parseCliArgs() would correctly identify the first positional. Consider switching subcommand detection to use parsed positionals[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 thread src/cli/help.ts Outdated
Comment thread src/daemon/ipcClient.ts
Comment thread src/daemon/daemonManager.ts Outdated
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 thread src/daemon/daemonChild.ts Outdated
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 thread src/daemon/serviceInstaller.ts Outdated
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 thread src/daemon/ipcServer.ts Outdated
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 thread src/daemon/daemonChild.ts Outdated
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 thread src/daemon/serviceInstaller.ts Outdated
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 thread src/daemon/serviceInstaller.ts Outdated
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"
Niladri2003 and others added 6 commits May 14, 2026 17:05
…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>
…ing and help messages and removed unused codes
Niladri2003 and others added 2 commits May 15, 2026 00:29
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants