Skip to content
This repository was archived by the owner on Apr 6, 2026. It is now read-only.

Commit fa8fa65

Browse files
pirumuclaude
andcommitted
feat: mac-client manages relay-server lifecycle
relay-server is no longer a separate launchd service. mac-client now spawns it on start and kills it (along with cloudflared) on quit or SIGTERM/SIGINT. Updated install script, build-bundle, CI workflow, and Homebrew cask to bundle relay-server inside the .app. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b66b43b commit fa8fa65

6 files changed

Lines changed: 98 additions & 47 deletions

File tree

.github/workflows/release.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,12 @@ jobs:
6262
- name: Create .app bundle
6363
run: |
6464
BINARY_PATH="mac-client/target/${{ matrix.target }}/release/mac-client"
65+
RELAY_PATH="relay-server/target/${{ matrix.target }}/release/relay-server"
6566
APP_NAME="Terminal Remote.app"
6667
mkdir -p "$APP_NAME/Contents/MacOS"
6768
mkdir -p "$APP_NAME/Contents/Resources"
6869
cp "$BINARY_PATH" "$APP_NAME/Contents/MacOS/"
70+
cp "$RELAY_PATH" "$APP_NAME/Contents/MacOS/"
6971
cp mac-client/Info.plist "$APP_NAME/Contents/"
7072
if [ -f mac-client/resources/AppIcon.icns ]; then
7173
cp mac-client/resources/AppIcon.icns "$APP_NAME/Contents/Resources/"
@@ -80,10 +82,10 @@ jobs:
8082
STAGING="terminal-remote-staging"
8183
mkdir -p "$STAGING/shell-integration"
8284
83-
# Copy .app bundle
85+
# Copy .app bundle (includes relay-server in Contents/MacOS/)
8486
cp -R "Terminal Remote.app" "$STAGING/"
8587
86-
# Copy relay-server binary
88+
# Copy relay-server binary (also needed standalone for ~/.terminal-remote/bin/)
8789
cp "relay-server/target/${{ matrix.target }}/release/relay-server" "$STAGING/relay-server"
8890
8991
# Copy pty-proxy binary

homebrew/Casks/terminal-remote.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
# Install the .app bundle
1818
app "Terminal Remote.app"
1919

20-
# Install binaries
21-
binary "relay-server", target: "#{HOMEBREW_PREFIX}/bin/terminal-remote-relay"
20+
# Install binaries (relay-server is bundled inside the .app, managed by mac-client)
2221
binary "pty-proxy", target: "#{HOMEBREW_PREFIX}/bin/terminal-remote-pty-proxy"
2322

2423
# Post-install: copy shell integration scripts and pty-proxy to install dir

mac-client/build-bundle.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ mkdir -p "$APP_PATH/Contents/Resources"
4242
echo "Copying binary..."
4343
cp "$BINARY_PATH" "$APP_PATH/Contents/MacOS/"
4444

45+
# Copy relay-server binary (mac-client manages its lifecycle)
46+
RELAY_BINARY="$PROJECT_ROOT/relay-server/target/release/relay-server"
47+
if [ -f "$RELAY_BINARY" ]; then
48+
echo "Copying relay-server..."
49+
cp "$RELAY_BINARY" "$APP_PATH/Contents/MacOS/"
50+
else
51+
echo "Warning: relay-server binary not found at $RELAY_BINARY (build with: cargo build --release -p relay-server)"
52+
fi
53+
4554
# Copy Info.plist
4655
echo "Copying Info.plist..."
4756
cp "$SCRIPT_DIR/Info.plist" "$APP_PATH/Contents/"

mac-client/src/main.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ struct App {
5151
copy_reset_time: Option<Instant>,
5252
pty_cmd_tx: Option<tokio::sync::mpsc::UnboundedSender<PtyCommand>>,
5353
cloudflared_pid: Arc<AtomicU32>,
54+
relay_server_pid: Arc<AtomicU32>,
5455
}
5556

5657
impl App {
@@ -65,6 +66,7 @@ impl App {
6566
copy_reset_time: None,
6667
pty_cmd_tx: None,
6768
cloudflared_pid: Arc::new(AtomicU32::new(0)),
69+
relay_server_pid: Arc::new(AtomicU32::new(0)),
6870
}
6971
}
7072

@@ -130,6 +132,11 @@ impl App {
130132
info!("Killing cloudflared (pid {})", pid);
131133
unsafe { libc::kill(pid as i32, libc::SIGTERM); }
132134
}
135+
let relay_pid = self.relay_server_pid.load(Ordering::Relaxed);
136+
if relay_pid != 0 {
137+
info!("Killing relay-server (pid {})", relay_pid);
138+
unsafe { libc::kill(relay_pid as i32, libc::SIGTERM); }
139+
}
133140
std::process::exit(0);
134141
}
135142
_ => {
@@ -312,9 +319,75 @@ fn main() {
312319
// Create pty command channel (sender stays in main thread)
313320
let (pty_cmd_tx, pty_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<PtyCommand>();
314321

322+
// Spawn relay-server as a child process
323+
let relay_server_pid = Arc::new(AtomicU32::new(0));
324+
{
325+
// Find relay-server binary: next to our binary, or in ~/.terminal-remote/bin/
326+
let relay_bin = std::env::current_exe()
327+
.ok()
328+
.and_then(|p| p.parent().map(|d| d.join("relay-server")))
329+
.filter(|p| p.exists())
330+
.or_else(|| {
331+
let home = std::env::var("HOME").ok()?;
332+
let p = std::path::PathBuf::from(home).join(".terminal-remote/bin/relay-server");
333+
p.exists().then_some(p)
334+
});
335+
336+
match relay_bin {
337+
Some(bin) => {
338+
info!("Starting relay-server from: {}", bin.display());
339+
match Command::new(&bin).spawn() {
340+
Ok(child) => {
341+
let pid = child.id();
342+
info!("relay-server started (pid {})", pid);
343+
relay_server_pid.store(pid, Ordering::Relaxed);
344+
}
345+
Err(e) => {
346+
error!("Failed to spawn relay-server: {}", e);
347+
}
348+
}
349+
}
350+
None => {
351+
warn!("relay-server binary not found, assuming it is already running");
352+
}
353+
}
354+
}
355+
315356
// Shared PID for killing cloudflared on quit
316357
let cloudflared_pid = Arc::new(AtomicU32::new(0));
317358

359+
// Install signal handler so relay-server and cloudflared are killed
360+
// even if mac-client is terminated via SIGTERM/SIGINT (e.g. launchctl stop)
361+
{
362+
let relay_pid = relay_server_pid.clone();
363+
let cf_pid = cloudflared_pid.clone();
364+
unsafe {
365+
let cleanup = move || {
366+
let rpid = relay_pid.load(Ordering::Relaxed);
367+
if rpid != 0 {
368+
libc::kill(rpid as i32, libc::SIGTERM);
369+
}
370+
let cpid = cf_pid.load(Ordering::Relaxed);
371+
if cpid != 0 {
372+
libc::kill(cpid as i32, libc::SIGTERM);
373+
}
374+
libc::_exit(0);
375+
};
376+
// Store in a static so the closure lives forever
377+
static mut CLEANUP: Option<Box<dyn Fn()>> = None;
378+
CLEANUP = Some(Box::new(cleanup));
379+
extern "C" fn handler(_sig: libc::c_int) {
380+
unsafe {
381+
if let Some(ref f) = CLEANUP {
382+
f();
383+
}
384+
}
385+
}
386+
libc::signal(libc::SIGTERM, handler as libc::sighandler_t);
387+
libc::signal(libc::SIGINT, handler as libc::sighandler_t);
388+
}
389+
}
390+
318391
// Spawn background thread with Tokio runtime
319392
let ui_tx_bg = ui_tx.clone();
320393
let cloudflared_pid_bg = cloudflared_pid.clone();
@@ -413,6 +486,7 @@ fn main() {
413486
app.bg_handle = Some(bg_handle);
414487
app.pty_cmd_tx = Some(pty_cmd_tx);
415488
app.cloudflared_pid = cloudflared_pid;
489+
app.relay_server_pid = relay_server_pid;
416490

417491
info!("Entering main event loop");
418492

scripts/install.sh

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ echo -e "${GREEN} pty-proxy -> $INSTALL_DIR/bin/pty-proxy${NC}"
142142
# Install .app bundle (remove old version first for clean update)
143143
rm -rf "$APP_DIR/$APP_NAME"
144144
cp -R "$TMPDIR/$APP_NAME" "$APP_DIR/"
145-
echo -e "${GREEN} $APP_NAME -> $APP_DIR/$APP_NAME${NC}"
145+
# Copy relay-server into the .app bundle (mac-client manages its lifecycle)
146+
cp "$INSTALL_DIR/bin/relay-server" "$APP_DIR/$APP_NAME/Contents/MacOS/relay-server"
147+
echo -e "${GREEN} $APP_NAME -> $APP_DIR/$APP_NAME (includes relay-server)${NC}"
146148

147149
# ── Install shell integration ─────────────────────────────────
148150
echo ""
@@ -223,47 +225,14 @@ fi
223225

224226
# Unload current agents if already installed (for clean reinstall)
225227
launchctl unload "$RELAY_PLIST" 2>/dev/null || true
228+
rm -f "$RELAY_PLIST" # relay-server is now managed by mac-client
226229
launchctl unload "$APP_PLIST" 2>/dev/null || true
227230

228231
# Build PATH that includes Homebrew so services can find cloudflared
229232
BREW_PREFIX="$(brew --prefix)"
230233
LAUNCH_PATH="${BREW_PREFIX}/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
231234

232-
# relay-server daemon
233-
cat > "$RELAY_PLIST" << EOF
234-
<?xml version="1.0" encoding="UTF-8"?>
235-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
236-
<plist version="1.0">
237-
<dict>
238-
<key>Label</key>
239-
<string>${RELAY_LABEL}</string>
240-
<key>ProgramArguments</key>
241-
<array>
242-
<string>${INSTALL_DIR}/bin/relay-server</string>
243-
</array>
244-
<key>EnvironmentVariables</key>
245-
<dict>
246-
<key>PATH</key>
247-
<string>${LAUNCH_PATH}</string>
248-
</dict>
249-
<key>RunAtLoad</key>
250-
<true/>
251-
<key>KeepAlive</key>
252-
<dict>
253-
<key>SuccessfulExit</key>
254-
<false/>
255-
</dict>
256-
<key>StandardOutPath</key>
257-
<string>${INSTALL_DIR}/relay-server.log</string>
258-
<key>StandardErrorPath</key>
259-
<string>${INSTALL_DIR}/relay-server.log</string>
260-
</dict>
261-
</plist>
262-
EOF
263-
264-
echo -e "${GREEN} Created $RELAY_PLIST${NC}"
265-
266-
# mac-client menu bar app
235+
# mac-client menu bar app (also starts/stops relay-server)
267236
cat > "$APP_PLIST" << EOF
268237
<?xml version="1.0" encoding="UTF-8"?>
269238
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -299,10 +268,9 @@ EOF
299268

300269
echo -e "${GREEN} Created $APP_PLIST${NC}"
301270

302-
# Start both services immediately
303-
launchctl load "$RELAY_PLIST"
271+
# Start service (mac-client manages relay-server lifecycle)
304272
launchctl load "$APP_PLIST"
305-
echo -e "${GREEN} Services started${NC}"
273+
echo -e "${GREEN} Service started${NC}"
306274

307275
# ── Summary ───────────────────────────────────────────────────
308276
echo ""
@@ -311,12 +279,11 @@ echo " Installation complete!"
311279
echo "========================================${NC}"
312280
echo ""
313281
echo " Installed:"
314-
echo " App: $APP_DIR/$APP_NAME"
315-
echo " Relay: $INSTALL_DIR/bin/relay-server"
282+
echo " App: $APP_DIR/$APP_NAME (includes relay-server)"
316283
echo " PTY Proxy: $INSTALL_DIR/bin/pty-proxy"
317284
echo " Shell config: $INSTALL_DIR/init.$SHELL_EXT"
318285
echo ""
319-
echo " Services are running and will auto-start on login."
286+
echo " Service is running and will auto-start on login."
320287
echo " Restart your shell to activate shell integration,"
321288
echo " then new terminal sessions will auto-register."
322289
echo ""

scripts/uninstall.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ echo " Removed:"
151151
echo " - App bundle ($APP_DIR/$APP_NAME)"
152152
echo " - Install directory ($INSTALL_DIR/)"
153153
echo " - Shell integration source lines"
154-
echo " - LaunchAgents (relay-server + mac-client)"
154+
echo " - LaunchAgent (mac-client, which manages relay-server)"
155155
echo " - Unix socket"
156156
echo ""
157157
echo " Restart your shell or open a new terminal to"

0 commit comments

Comments
 (0)