package main import ( "encoding/json" "fmt" "log" "net" "os" "time" ) const sockPath = "/tmp/exchange-monitor.sock" // IPCCommand is sent from CLI client to daemon. type IPCCommand struct { Action string `json:"action"` // status, close-all, close, stop, start Coin string `json:"coin,omitempty"` } // IPCResponse is sent back from daemon to CLI client. type IPCResponse struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } // startIPCServer starts the Unix socket listener for CLI commands. func (t *Trader) startIPCServer() { os.Remove(sockPath) // clean up stale socket ln, err := net.Listen("unix", sockPath) if err != nil { log.Printf("[IPC] Failed to create socket: %v", err) return } log.Printf("[IPC] Listening on %s", sockPath) go func() { defer ln.Close() for { conn, err := ln.Accept() if err != nil { continue } go t.handleIPC(conn) } }() } func (t *Trader) handleIPC(conn net.Conn) { defer conn.Close() conn.SetDeadline(time.Now().Add(5 * time.Second)) var cmd IPCCommand if err := json.NewDecoder(conn).Decode(&cmd); err != nil { json.NewEncoder(conn).Encode(IPCResponse{Success: false, Error: "invalid command: " + err.Error()}) return } var resp IPCResponse switch cmd.Action { case "status": positions := t.ReadSnapshot() c, d, f, tot := t.GetClosedStats() resp = IPCResponse{Success: true, Data: map[string]interface{}{ "positions": positions, "converged": c, "diverged": d, "flat": f, "total": tot, }} case "close-all": count := t.CloseAllPositions() resp = IPCResponse{Success: true, Data: map[string]interface{}{ "closed": count, "message": fmt.Sprintf("Closed %d positions", count), }} case "close": if cmd.Coin == "" { resp = IPCResponse{Success: false, Error: "missing coin name"} } else if err := t.ClosePosition(cmd.Coin); err != nil { resp = IPCResponse{Success: false, Error: err.Error()} } else { resp = IPCResponse{Success: true, Data: map[string]string{"closed": cmd.Coin}} } case "stop": t.Stop() resp = IPCResponse{Success: true, Data: map[string]string{"status": "stopped"}} case "start": t.Start() resp = IPCResponse{Success: true, Data: map[string]string{"status": "started"}} default: resp = IPCResponse{Success: false, Error: "unknown action: " + cmd.Action} } json.NewEncoder(conn).Encode(resp) } // runIPCClient sends a command to the running daemon and prints the response. func runIPCClient(action, coin string) { conn, err := net.DialTimeout("unix", sockPath, 2*time.Second) if err != nil { fmt.Fprintf(os.Stderr, "Error: daemon not running? (%v)\n", err) os.Exit(1) } defer conn.Close() cmd := IPCCommand{Action: action, Coin: coin} if err := json.NewEncoder(conn).Encode(cmd); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } var resp IPCResponse if err := json.NewDecoder(conn).Decode(&resp); err != nil { fmt.Fprintf(os.Stderr, "Error reading response: %v\n", err) os.Exit(1) } if !resp.Success { fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Error) os.Exit(1) } // Pretty-print response data, _ := json.MarshalIndent(resp.Data, "", " ") fmt.Println(string(data)) }