Skip to content

Commit 2afcd79

Browse files
committed
feat: completely new approach
Instead of building new images, we just run them with a tiny mount that contains a statically-linked binary that wraps the entry point and launches a signaling ws server.
1 parent 51be5fc commit 2afcd79

16 files changed

+296
-357
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ node_modules/
22
test-results/
33
lib/
44
types/
5+
deadmanswitch/bin/*
6+
deadmanswitch/deadmanswitch

.npmignore

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
*
2-
!lib/
32
!lib/**
4-
!types/
53
!types/**
6-
!docker/
7-
!docker/**
4+
!deadmanswitch/bin/*

deadmanswitch/build.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
set -e
3+
set +x
4+
5+
trap "cd $(pwd -P)" EXIT
6+
cd $(dirname "$0")
7+
8+
rm -rf ./bin
9+
mkdir bin
10+
11+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/deadmanswitch_linux_x86_64
12+
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/deadmanswitch_linux_aarch64
13+
14+
# https://upx.github.io/ - 30% less file size.
15+
upx --best --ultra-brute bin/*

deadmanswitch/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module deadmanswitch
2+
3+
go 1.21.3
4+
5+
require github.com/gorilla/websocket v1.5.3

deadmanswitch/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
2+
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

deadmanswitch/main.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
"os"
8+
"os/exec"
9+
"os/signal"
10+
"strconv"
11+
"sync"
12+
"syscall"
13+
"time"
14+
15+
"github.com/gorilla/websocket"
16+
)
17+
18+
var upgrader = websocket.Upgrader{
19+
ReadBufferSize: 1024,
20+
WriteBufferSize: 1024,
21+
CheckOrigin: func(r *http.Request) bool {
22+
return true // Allow all connections for this example
23+
},
24+
}
25+
26+
func printUsage() {
27+
fmt.Println("Usage: process_manager <command-to-run>")
28+
fmt.Println("Example: process_manager python3 script.py")
29+
}
30+
31+
func getEnvWithDefault(key, defaultValue string) string {
32+
if value, exists := os.LookupEnv(key); exists {
33+
return value
34+
}
35+
return defaultValue
36+
}
37+
38+
func main() {
39+
if len(os.Args) <= 1 {
40+
printUsage()
41+
os.Exit(1)
42+
}
43+
44+
// Parse command line arguments
45+
command := os.Args[1]
46+
args := os.Args[2:]
47+
48+
// Get configuration from environment variables
49+
port, _ := strconv.Atoi(getEnvWithDefault("DEADMANSWITCH_PORT", "54321"))
50+
wsSuffix := getEnvWithDefault("DEADMANSWITCH_SUFFIX", "")
51+
52+
connectionTimeoutSeconds, _ := strconv.Atoi(getEnvWithDefault("DEADMANSWITCH_TIMEOUT", "10"))
53+
54+
// Channel to coordinate shutdown
55+
cmdFinished := make(chan struct{})
56+
clientConnected := make(chan struct{})
57+
clientDisconnected := make(chan struct{})
58+
59+
// Start the child process
60+
cmd := exec.Command(command, args...)
61+
cmd.Stdout = os.Stdout
62+
cmd.Stderr = os.Stderr
63+
cmd.Stdin = os.Stdin
64+
65+
if err := cmd.Start(); err != nil {
66+
log.Fatalf("Failed to start process: %v", err)
67+
}
68+
69+
// Handle process completion
70+
go func() {
71+
cmd.Wait()
72+
close(cmdFinished)
73+
}()
74+
75+
// Setup WebSocket handler
76+
var mutex sync.Mutex
77+
has_client := false
78+
http.HandleFunc("/"+wsSuffix, func(w http.ResponseWriter, r *http.Request) {
79+
var had_clients bool
80+
mutex.Lock()
81+
had_clients = has_client
82+
mutex.Unlock()
83+
84+
if had_clients {
85+
http.NotFound(w, r)
86+
return
87+
}
88+
conn, err := upgrader.Upgrade(w, r, nil)
89+
if err != nil {
90+
log.Printf("[deadmanswitch] Failed to upgrade connection: %v", err)
91+
return
92+
}
93+
defer conn.Close()
94+
95+
mutex.Lock()
96+
has_client = true
97+
mutex.Unlock()
98+
99+
close(clientConnected)
100+
101+
for {
102+
_, _, err := conn.ReadMessage()
103+
if err != nil {
104+
// Break the loop if an error occurs (e.g., if the connection is closed)
105+
close(clientDisconnected)
106+
break
107+
}
108+
}
109+
})
110+
111+
// Start HTTP server
112+
server := &http.Server{
113+
Addr: fmt.Sprintf(":%d", port),
114+
}
115+
116+
go func() {
117+
if err := server.ListenAndServe(); err != http.ErrServerClosed {
118+
log.Printf("[deadmanswitch] Server error: %v", err)
119+
}
120+
}()
121+
122+
log.Printf("[deadmanswitch] Waiting %d seconds on ws://localhost:%d/%s for client", connectionTimeoutSeconds, port, wsSuffix)
123+
// Handle OS signals
124+
sigChan := make(chan os.Signal, 1)
125+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
126+
127+
// Set up connection timeout
128+
connetionTimer := time.NewTimer(time.Duration(connectionTimeoutSeconds) * time.Second)
129+
go func() {
130+
log.Printf("[deadmanswitch] expecting client connect...")
131+
<-clientConnected
132+
log.Printf("[deadmanswitch] yeah client connected")
133+
connetionTimer.Stop()
134+
}()
135+
136+
select {
137+
case <-connetionTimer.C:
138+
log.Printf("[deadmanswitch] still no client after %d seconds - closing.", connectionTimeoutSeconds)
139+
case sig := <-sigChan:
140+
log.Printf("[deadmanswitch] Received %v, exiting", sig)
141+
case <-clientDisconnected:
142+
log.Println("[deadmanswitch] client disconnected, terminating process")
143+
case <-cmdFinished:
144+
log.Println("[deadmanswitch] process finished")
145+
}
146+
147+
log.Println("[deadmanswitch] Shutting down...")
148+
149+
// Shutdown the server
150+
server.Close()
151+
connetionTimer.Stop()
152+
153+
// Give the process a chance to terminate gracefully
154+
if cmd.Process != nil {
155+
processShutdownTimer := time.NewTimer(time.Duration(connectionTimeoutSeconds) * time.Second)
156+
cmd.Process.Signal(syscall.SIGTERM)
157+
select {
158+
case <-processShutdownTimer.C:
159+
cmd.Process.Kill()
160+
case <-cmdFinished:
161+
processShutdownTimer.Stop()
162+
}
163+
}
164+
}

docker/build.sh

Lines changed: 0 additions & 48 deletions
This file was deleted.

docker/deadmanswitch.dockerfile

Lines changed: 0 additions & 14 deletions
This file was deleted.

docker/deb_setup_23.x

Lines changed: 0 additions & 113 deletions
This file was deleted.

0 commit comments

Comments
 (0)