Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,24 @@

Download the latest release for your platform and run it:

```
```bash
./hyscale
```

**Run in background** (detached mode):

```bash
./hyscale -detach
# or short form
./hyscale -d
```

When running in detached mode:

- Logs are written to `hyscale.log` in the same directory
- Stop with: `./hyscale -stop`
- The process continues running even if you close the terminal

### 2. Authenticate with Tailscale

On first run, you'll see an authentication URL:
Expand Down
168 changes: 168 additions & 0 deletions cmd/hyscale/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"embed"
"flag"
"fmt"
"io/fs"
"log"
"net"
Expand Down Expand Up @@ -43,9 +44,24 @@ func main() {
stateDir = flag.String("state-dir", "", "Tailscale state directory (default: tsnet default)")
serverDir = flag.String("server-dir", "server", "Hytale server directory")
noTLS = flag.Bool("notls", false, "Disable TLS (use HTTP on port 80 instead of HTTPS on 443)")
detach = flag.Bool("detach", false, "Run in background (detached mode)")
detachShort = flag.Bool("d", false, "Run in background (shortcut for -detach)")
stop = flag.Bool("stop", false, "Stop a running detached instance")
)
flag.Parse()

// Handle -stop flag
if *stop {
handleStop(*configPath)
return
}

// Handle -detach flag
if *detach || *detachShort {
handleDetach(*configPath)
return
}

// Resolve absolute path for config
absConfigPath, err := filepath.Abs(*configPath)
if err != nil {
Expand Down Expand Up @@ -389,6 +405,10 @@ authenticated:
// Close log manager
logMgr.Close()

// Remove PID file if it exists
pidFile := getPidFilePath(absConfigPath)
os.Remove(pidFile)

// Shutdown HTTP server
httpServer.Shutdown(context.Background())
}()
Expand All @@ -401,3 +421,151 @@ authenticated:

log.Println("Goodbye!")
}

// getPidFilePath returns the path to the PID file based on config path
func getPidFilePath(configPath string) string {
absPath, _ := filepath.Abs(configPath)
dir := filepath.Dir(absPath)
return filepath.Join(dir, ".hyscale.pid")
}

// handleDetach forks the process to run in background
func handleDetach(configPath string) {
pidFile := getPidFilePath(configPath)

// Check if already running
if pid, err := os.ReadFile(pidFile); err == nil {
log.Printf("hyscale appears to be already running (PID %s)", string(pid))
log.Printf("If not, remove %s and try again", pidFile)
os.Exit(1)
}

// Prepare to re-execute ourselves
executable, err := os.Executable()
if err != nil {
log.Fatalf("Failed to get executable path: %v", err)
}

// Build args without -detach/-d flags
args := []string{}
skipNext := false
for i, arg := range os.Args[1:] {
if skipNext {
skipNext = false
continue
}
if arg == "-detach" || arg == "-d" || arg == "--detach" {
continue
}
if arg == "-config" || arg == "--config" {
args = append(args, arg)
skipNext = true
if i+1 < len(os.Args[1:]) {
args = append(args, os.Args[i+2])
}
continue
}
args = append(args, arg)
}

// Ensure config flag is present
hasConfig := false
for _, arg := range args {
if arg == "-config" || arg == "--config" {
hasConfig = true
break
}
}
if !hasConfig {
args = append([]string{"-config", configPath}, args...)
}

// Set up log file
logDir := filepath.Dir(pidFile)
logFile := filepath.Join(logDir, "hyscale.log")
outFile, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
}

// Start the process
attr := &os.ProcAttr{
Files: []*os.File{nil, outFile, outFile}, // stdin=nil, stdout=logFile, stderr=logFile
}

process, err := os.StartProcess(executable, append([]string{executable}, args...), attr)
if err != nil {
outFile.Close()
log.Fatalf("Failed to start background process: %v", err)
}

// Write PID file
if err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", process.Pid)), 0644); err != nil {
process.Kill()
outFile.Close()
log.Fatalf("Failed to write PID file: %v", err)
}

outFile.Close()

log.Printf("hyscale started in background (PID %d)", process.Pid)
log.Printf("Logs: %s", logFile)
log.Printf("Stop with: %s -stop", os.Args[0])
}

// handleStop stops a running detached instance
func handleStop(configPath string) {
pidFile := getPidFilePath(configPath)

// Read PID file
pidData, err := os.ReadFile(pidFile)
if err != nil {
if os.IsNotExist(err) {
log.Printf("No running instance found (no PID file at %s)", pidFile)
} else {
log.Fatalf("Failed to read PID file: %v", err)
}
os.Exit(1)
}

var pid int
if _, err := fmt.Sscanf(string(pidData), "%d", &pid); err != nil {
log.Fatalf("Invalid PID file content: %v", err)
}

// Find the process
process, err := os.FindProcess(pid)
if err != nil {
log.Fatalf("Failed to find process: %v", err)
}

// Send interrupt signal (SIGINT on Unix, similar on Windows)
log.Printf("Stopping hyscale (PID %d)...", pid)
if err := process.Signal(syscall.SIGINT); err != nil {
// Try SIGTERM if SIGINT fails
if err := process.Signal(syscall.SIGTERM); err != nil {
log.Fatalf("Failed to stop process: %v", err)
}
}

// Wait a bit for graceful shutdown
time.Sleep(2 * time.Second)

// Check if process is still running
if err := process.Signal(syscall.Signal(0)); err == nil {
log.Printf("Process still running, waiting...")
time.Sleep(3 * time.Second)

// Check again
if err := process.Signal(syscall.Signal(0)); err == nil {
log.Printf("Warning: Process may still be running. You may need to kill it manually.")
}
}

// Remove PID file
if err := os.Remove(pidFile); err != nil && !os.IsNotExist(err) {
log.Printf("Warning: Failed to remove PID file: %v", err)
}

log.Printf("hyscale stopped")
}