-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
191 lines (161 loc) · 4.55 KB
/
Copy pathmain.go
File metadata and controls
191 lines (161 loc) · 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// Copyright 2024 Drycc Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"sync"
"syscall"
"time"
"github.com/spf13/cobra"
)
var (
bind string
interval time.Duration
pingMutex sync.RWMutex
lastPingTime time.Time
)
var rootCmd = &cobra.Command{
Use: "pingguard [flags] -- [program args...]",
Short: "Start any program with ping health check functionality",
Long: `pingguard is a wrapper program that can:
1. Start any program
2. Provide ping health check functionality, auto-exit if no ping requests received within specified time
Usage examples:
pingguard --interval=60s --bind=127.0.0.1:8081 -- python -m http.server 8000`,
Run: runPingguard,
}
func init() {
rootCmd.Flags().StringVar(&bind, "bind", "127.0.0.1:8081", "ping service bind address and port (format: host:port)")
rootCmd.Flags().DurationVar(&interval, "interval", 60*time.Second, "ping timeout interval, program will exit if no ping requests received within this time")
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runPingguard(_ *cobra.Command, args []string) {
// Initialize ping time
updatePingTime()
// Start ping HTTP server
server := startPingServer()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("ping server shutdown error: %v", err)
}
}()
// Start target program process
targetCmd := startTargetProgram(args)
defer func() {
if targetCmd.Process != nil {
log.Println("terminating target process...")
if err := targetCmd.Process.Signal(os.Interrupt); err != nil {
log.Printf("failed to send interrupt signal: %v", err)
_ = targetCmd.Process.Kill()
}
}
}()
// Start ping checker goroutine
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go pingChecker(ctx)
// Wait for signal or process termination
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
procDone := make(chan error, 1)
go func() {
procDone <- targetCmd.Wait()
}()
select {
case sig := <-sigChan:
log.Printf("received signal %v, exiting...", sig)
case err := <-procDone:
if err != nil {
log.Printf("target process exited with error: %v", err)
} else {
log.Println("target process exited normally")
}
}
log.Println("program exited")
}
func startPingServer() *http.Server {
mux := http.NewServeMux()
mux.HandleFunc("/_/ping", pingHandler)
server := &http.Server{
Addr: bind,
Handler: mux,
}
go func() {
log.Printf("ping server started on %s", bind)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("failed to start ping server: %v", err)
}
}()
return server
}
func pingHandler(w http.ResponseWriter, r *http.Request) {
updatePingTime()
w.WriteHeader(http.StatusOK)
w.Write([]byte("pong"))
log.Printf("received ping request from %s", r.RemoteAddr)
}
func updatePingTime() {
pingMutex.Lock()
lastPingTime = time.Now()
pingMutex.Unlock()
}
func getLastPingTime() time.Time {
pingMutex.RLock()
defer pingMutex.RUnlock()
return lastPingTime
}
func pingChecker(ctx context.Context) {
ticker := time.NewTicker(interval / 3) // Check frequency is 1/3 of timeout interval
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if time.Since(getLastPingTime()) > interval {
log.Printf("no ping requests received for %v, program auto-exiting", interval)
// Send SIGTERM signal to self
p, _ := os.FindProcess(os.Getpid())
_ = p.Signal(syscall.SIGTERM)
return
}
}
}
}
func startTargetProgram(args []string) *exec.Cmd {
if len(args) == 0 {
log.Fatal("error: must provide program and arguments to execute")
}
// First argument is program name, rest are arguments
programName := args[0]
programArgs := args[1:]
cmd := exec.Command(programName, programArgs...)
// Connect standard input/output
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
log.Printf("starting command: %s %v", programName, programArgs)
if err := cmd.Start(); err != nil {
log.Fatalf("failed to start program: %v", err)
}
log.Printf("target process started successfully, PID: %d", cmd.Process.Pid)
return cmd
}