@@ -2,6 +2,7 @@ package main
22
33import (
44 "encoding/json"
5+ "flag"
56 "fmt"
67 "log"
78 "net"
@@ -15,6 +16,10 @@ import (
1516 "github.com/hashicorp/memberlist"
1617)
1718
19+ type cliOptions struct {
20+ List bool
21+ }
22+
1823const (
1924 envDB = "PSSTD_DB"
2025 envHTTP = "PSSTD_HTTP"
@@ -23,16 +28,22 @@ const (
2328 envHTTPAd = "PSSTD_ADVERTISE_HTTP"
2429 envWeb = "PSSTD_WEB" // "true" to enable HTTP, default true
2530 envNodeName = "PSSTD_NODE_NAME"
31+ envNodeTTL = "PSSTD_NODE_TTL"
2632 gossipPort = 7946
2733 httpPort = 8080
2834)
2935
3036func main () {
37+ opts := parseCLI (os .Args [1 :])
3138 hostname , _ := os .Hostname ()
3239 nodeName , err := nodeNameFromEnv (hostname )
3340 if err != nil {
3441 log .Fatalf ("node name: %v" , err )
3542 }
43+ nodeTTL , err := nodeTTLFromEnv ()
44+ if err != nil {
45+ log .Fatalf ("node ttl: %v" , err )
46+ }
3647
3748 dbPath := envOr (envDB , "./data" )
3849 httpAddr := envOr (envHTTP , fmt .Sprintf (":%d" , httpPort ))
@@ -52,7 +63,7 @@ func main() {
5263 if err != nil {
5364 if pebbleLockHeld (err ) {
5465 log .Printf ("psstd already appears to own %s; starting terminal mirror instead" , dbPath )
55- runTerminalMirror (nodeName , gossipAddr , seeds )
66+ runTerminalMirror (nodeName , gossipAddr , seeds , opts . List )
5667 return
5768 }
5869 log .Fatalf ("pebble open: %v" , err )
@@ -84,7 +95,7 @@ func main() {
8495 }
8596 db = nil
8697 log .Printf ("psstd already appears to be listening on %s; starting terminal mirror instead" , gossipAddr )
87- runTerminalMirror (nodeName , gossipAddr , seeds )
98+ runTerminalMirror (nodeName , gossipAddr , seeds , opts . List )
8899 return
89100 }
90101 log .Fatalf ("memberlist create: %v" , err )
@@ -99,29 +110,28 @@ func main() {
99110 // 2. Scan for existing peers (mDNS + any explicit seeds)
100111 discovered := discoverPeers ()
101112 allSeeds := append (seeds , discovered ... )
102- logStartupConfig (startupConfig {
103- NodeName : nodeName ,
104- DBPath : dbPath ,
105- HTTPAddr : httpAddr ,
106- WebURL : webURL ,
107- GossipAddr : gossipAddr ,
108- WebEnabled : webEnabled ,
109- Version : appVersion ,
110- SeedCount : len (seeds ),
111- MDNSCount : len (discovered ),
112- })
113+ joinedPeers := 0
114+ joinErr := error (nil )
113115 if len (allSeeds ) > 0 {
114- if n , err := list .Join (allSeeds ); err != nil {
115- log .Printf ("join warning (joined %d): %v" , n , err )
116- } else {
117- log .Printf ("joined cluster, %d peer(s)" , n )
118- }
119- } else {
120- log .Println ("no peers found — running solo, will be discovered by others" )
116+ joinedPeers , joinErr = list .Join (allSeeds )
121117 }
118+ logStartupConfig (startupConfig {
119+ NodeName : nodeName ,
120+ DBPath : dbPath ,
121+ HTTPAddr : httpAddr ,
122+ WebURL : webURL ,
123+ GossipAddr : gossipAddr ,
124+ WebEnabled : webEnabled ,
125+ Version : appVersion ,
126+ NodeTTL : nodeTTL ,
127+ SeedCount : len (seeds ),
128+ MDNSCount : len (discovered ),
129+ JoinedPeers : joinedPeers ,
130+ JoinErr : joinErr ,
131+ })
122132
123133 // ── Stats heartbeat ─────────────────────────────────────────────────────
124- go statsLoop (nodeName , webURL , appVersion , db , delegate )
134+ go statsLoop (nodeName , webURL , appVersion , nodeTTL , db , delegate )
125135
126136 // ── HTTP ─────────────────────────────────────────────────────────────────
127137 if webEnabled {
@@ -135,21 +145,44 @@ func main() {
135145 }
136146}
137147
148+ func parseCLI (args []string ) cliOptions {
149+ fs := flag .NewFlagSet ("psstd" , flag .ExitOnError )
150+ fs .SetOutput (os .Stderr )
151+ var opts cliOptions
152+ fs .BoolVar (& opts .List , "l" , false , "render terminal mirror as a vertical node list" )
153+ fs .BoolVar (& opts .List , "list" , false , "render terminal mirror as a vertical node list" )
154+ _ = fs .Parse (args )
155+ return opts
156+ }
157+
138158type startupConfig struct {
139- NodeName string
140- DBPath string
141- HTTPAddr string
142- WebURL string
143- GossipAddr string
144- WebEnabled bool
145- Version string
146- SeedCount int
147- MDNSCount int
159+ NodeName string
160+ DBPath string
161+ HTTPAddr string
162+ WebURL string
163+ GossipAddr string
164+ WebEnabled bool
165+ Version string
166+ NodeTTL time.Duration
167+ SeedCount int
168+ MDNSCount int
169+ JoinedPeers int
170+ JoinErr error
148171}
149172
150173func logStartupConfig (cfg startupConfig ) {
151- log .Printf ("psstd startup: version=%s node=%s db=%s web=%t http=%s advertise=%s gossip=%s seeds=%d mdns=%d" ,
152- cfg .Version , cfg .NodeName , cfg .DBPath , cfg .WebEnabled , cfg .HTTPAddr , cfg .WebURL , cfg .GossipAddr , cfg .SeedCount , cfg .MDNSCount )
174+ log .Print (startupSummary (cfg ))
175+ }
176+
177+ func startupSummary (cfg startupConfig ) string {
178+ join := "solo"
179+ if cfg .JoinErr != nil {
180+ join = fmt .Sprintf ("warning joined=%d error=%q" , cfg .JoinedPeers , cfg .JoinErr )
181+ } else if cfg .JoinedPeers > 0 {
182+ join = fmt .Sprintf ("joined=%d" , cfg .JoinedPeers )
183+ }
184+ return fmt .Sprintf ("psstd startup: version=%s node=%s db=%s web=%t http=%s advertise=%s gossip=%s ttl=%s seeds=%d mdns=%d join=%s" ,
185+ cfg .Version , cfg .NodeName , cfg .DBPath , cfg .WebEnabled , cfg .HTTPAddr , cfg .WebURL , cfg .GossipAddr , cfg .NodeTTL , cfg .SeedCount , cfg .MDNSCount , join )
153186}
154187
155188func pebbleLockHeld (err error ) bool {
@@ -168,7 +201,7 @@ func addressInUse(err error) bool {
168201 strings .Contains (msg , "bind: only one usage of each socket address" )
169202}
170203
171- func runTerminalMirror (hostname , gossipAddr string , seeds []string ) {
204+ func runTerminalMirror (hostname , gossipAddr string , seeds []string , listMode bool ) {
172205 tmpDir , err := os .MkdirTemp ("" , "psstd-view-*" )
173206 if err != nil {
174207 log .Fatalf ("terminal mirror temp db: %v" , err )
@@ -203,7 +236,7 @@ func runTerminalMirror(hostname, gossipAddr string, seeds []string) {
203236 log .Printf ("terminal mirror joined cluster, %d peer(s)" , n )
204237 }
205238
206- terminalRenderLoop (db )
239+ terminalRenderLoop (db , listMode )
207240}
208241
209242func terminalMirrorSeeds (gossipAddr string , seeds []string ) []string {
@@ -219,11 +252,11 @@ func terminalMirrorSeeds(gossipAddr string, seeds []string) []string {
219252
220253// ── Stats loop ───────────────────────────────────────────────────────────────
221254
222- func statsLoop (hostname , webURL , version string , db * pebble.DB , d * kvDelegate ) {
255+ func statsLoop (hostname , webURL , version string , ttl time. Duration , db * pebble.DB , d * kvDelegate ) {
223256 ticker := time .NewTicker (2 * time .Second )
224257 defer ticker .Stop ()
225258 for range ticker .C {
226- stats , err := collectStats (hostname , webURL , version )
259+ stats , err := collectStats (hostname , webURL , version , ttl )
227260 if err != nil {
228261 log .Printf ("stats error: %v" , err )
229262 continue
@@ -303,6 +336,21 @@ func nodeNameFromEnv(hostname string) (string, error) {
303336 return override , nil
304337}
305338
339+ func nodeTTLFromEnv () (time.Duration , error ) {
340+ value , ok := os .LookupEnv (envNodeTTL )
341+ if ! ok || value == "" {
342+ return defaultNodeTTL , nil
343+ }
344+ ttl , err := time .ParseDuration (value )
345+ if err != nil {
346+ return 0 , fmt .Errorf ("%s must be a duration such as 15s or 1m: %w" , envNodeTTL , err )
347+ }
348+ if ttl < 2 * time .Second {
349+ return 0 , fmt .Errorf ("%s must be at least 2s" , envNodeTTL )
350+ }
351+ return ttl , nil
352+ }
353+
306354func splitCSV (s string ) []string {
307355 var out []string
308356 for _ , p := range strings .Split (s , "," ) {
0 commit comments