From 7ff97eda51779073be18b0381ecfbd8e48dc70a3 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 27 May 2026 16:33:16 -0700 Subject: [PATCH] Add restart endpoint Former-commit-id: 734542c3ed83ba5a681aaacfce607aae1b88f901 --- main.go | 22 +++++++++++++++------- reexec_unix.go | 21 +++++++++++++++++++++ reexec_windows.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 reexec_unix.go create mode 100644 reexec_windows.go diff --git a/main.go b/main.go index 2e50f27..3366f89 100644 --- a/main.go +++ b/main.go @@ -1032,6 +1032,19 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey( logger.Info("Tunnel destroyed, ready for reconnection") }) + client.RegisterHandler("newt/wg/restart", func(msg websocket.WSMessage) { + closeWgTunnel() + closeClients() + if healthMonitor != nil { + healthMonitor.Stop() + } + client.Close() + if err := reexec(); err != nil { + logger.Error("Failed to restart: %v", err) + os.Exit(1) + } + }) + client.RegisterHandler("newt/wg/terminate", func(msg websocket.WSMessage) { logger.Info("Received termination message") if wgData.PublicKey != "" { @@ -1968,13 +1981,8 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey( healthMonitor.Stop() } client.Close() - exe, exeErr := os.Executable() - if exeErr != nil { - logger.Error("Failed to get executable path for restart: %v", exeErr) - os.Exit(0) - } - if err := syscall.Exec(exe, os.Args, os.Environ()); err != nil { - logger.Error("Failed to re-exec for restart: %v", err) + if err := reexec(); err != nil { + logger.Error("Failed to restart: %v", err) os.Exit(1) } } diff --git a/reexec_unix.go b/reexec_unix.go new file mode 100644 index 0000000..b6c01cc --- /dev/null +++ b/reexec_unix.go @@ -0,0 +1,21 @@ +//go:build !windows + +package main + +import ( + "fmt" + "os" + "syscall" +) + +// reexec replaces the current process image with a fresh copy of itself, +// preserving all arguments and environment variables. On success it never +// returns (execve replaces the process in-place). On failure it returns an +// error describing why the exec could not be performed. +func reexec() error { + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + return syscall.Exec(exe, os.Args, os.Environ()) +} diff --git a/reexec_windows.go b/reexec_windows.go new file mode 100644 index 0000000..770f544 --- /dev/null +++ b/reexec_windows.go @@ -0,0 +1,30 @@ +//go:build windows + +package main + +import ( + "fmt" + "os" + "os/exec" +) + +// reexec spawns a new copy of the current process with the same arguments and +// environment, then exits the current process. On Windows, execve is not +// available, so we start a child process and exit. On success it never returns +// (os.Exit terminates the current process). On failure it returns an error. +func reexec() error { + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + cmd := exec.Command(exe, os.Args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Env = os.Environ() + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start new process: %w", err) + } + os.Exit(0) + return nil // unreachable +}