diff --git a/README.md b/README.md index e6c32ad..a651a10 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ For detailed API documentation, please see [docs/api.md](docs/api.md) - [x] Fix backend can still start frpc instance when it is already running - [ ] Develop an agent software to handle windows service management - [ ] Refactor all log output level to be more clear +- [ ] Add global websocket endpoint for posting notifications +- [ ] Add frpc instance watchdog ## License diff --git a/config.go b/config.go index 58ad055..0e96f40 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,9 @@ type Config struct { FrpcPath string `json:"frpcPath"` InstancePath string `json:"instancePath"` Debug bool `json:"debug"` + Watchdog struct { + Port int `json:"port"` + } `json:"watchdog"` } type InstanceInfo struct { @@ -75,13 +78,21 @@ func LoadConfig(configPath string) (*Config, error) { } if config.FrpcPath == "" { - config.FrpcPath = "/usr/bin/frpc" + if GetInitSystem() == "windows" { + config.FrpcPath = "frp_client/frpc.exe" + } else { + config.FrpcPath = "/usr/bin/frpc" + } } if config.InstancePath == "" { config.InstancePath = "./configs" } + if config.Watchdog.Port == 0 { + config.Watchdog.Port = 12380 + } + if err := os.MkdirAll(config.InstancePath, 0755); err != nil { return nil, fmt.Errorf("failed to create config directory: %w", err) } diff --git a/main.go b/main.go index 30b8474..bd29aba 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os/signal" "super-frpc/frpLogger" "super-frpc/postLog" + "super-frpc/watchdog" "syscall" "time" ) @@ -90,6 +91,17 @@ func main() { setupRoutes() + err = watchdog.Init() + if err != nil { + postLog.Error(fmt.Sprintf("Unable to initialize Watchdog: %s", err)) + } else { + if !watchdog.Connect("127.0.0.1", config.Watchdog.Port) { + postLog.Error(fmt.Sprintf("Failed to connect to Watchdog at %s:%d", "127.0.0.1", config.Watchdog.Port)) + } + } + + + addr := fmt.Sprintf("%s:%s", config.ListenAddr, config.ListenPort) server := &http.Server{ Addr: addr, diff --git a/router.go b/router.go index fc055d7..558e5da 100644 --- a/router.go +++ b/router.go @@ -8,7 +8,6 @@ import ( ) func setupRoutes() { - postLog.Info("Setting up routes...") http.HandleFunc("/system/getStatus", GetStatusHandler) http.HandleFunc("/system/getSoftwareInfo", GetSoftwareInfoHandler) systemLogHandler := postLog.NewLogSocketHandler(postLog.GetLogBroadcaster()) @@ -44,6 +43,8 @@ func setupRoutes() { http.HandleFunc("/", NotFoundHandler) + postLog.Info("Routes setup successfully") + } func NotFoundHandler(w http.ResponseWriter, r *http.Request) { diff --git a/watchdog/command.go b/watchdog/command.go new file mode 100644 index 0000000..20f4a35 --- /dev/null +++ b/watchdog/command.go @@ -0,0 +1,33 @@ +package watchdog + +import ( + "fmt" +) + +func AddInstance(serviceName string) bool { + if !IsConnected() { + return false + } + + message := fmt.Sprintf("[addInstance] %s", serviceName) + response, err := sendMsg(message, 3) + if err != nil { + return false + } + + return response == "success" +} + +func RemoveInstance(serviceName string) bool { + if !IsConnected() { + return false + } + + message := fmt.Sprintf("[removeInstance] %s", serviceName) + response, err := sendMsg(message, 3) + if err != nil { + return false + } + + return response == "success" +} diff --git a/watchdog/connect.go b/watchdog/connect.go new file mode 100644 index 0000000..b6d7f82 --- /dev/null +++ b/watchdog/connect.go @@ -0,0 +1,47 @@ +package watchdog + +import ( + "time" +) + +func Connect(ipaddr string, port int) bool { + if IsConnected() { + return true + } + + if err := Init(); err != nil { + return false + } + + if err := tcpConnect(ipaddr, port); err != nil { + return false + } + + response, err := sendMsg("watchdogAgentConnectionTest", 3) + if err != nil { + Destroy() + return false + } + + if response != "success" { + Destroy() + return false + } + + return true +} + +func Disconnect() bool { + if !IsConnected() { + return true + } + + err := Destroy() + if err != nil { + return false + } + + time.Sleep(100 * time.Millisecond) + + return true +} diff --git a/watchdog/tcpClient.go b/watchdog/tcpClient.go new file mode 100644 index 0000000..811a7b2 --- /dev/null +++ b/watchdog/tcpClient.go @@ -0,0 +1,164 @@ +package watchdog + +import ( + "bufio" + "fmt" + "net" + "super-frpc/postLog" + "sync" + "time" +) + +var ( + tcpConn net.Conn + tcpConnMutex sync.Mutex + isConnected bool + recvChan chan string + stopRecvChan chan struct{} +) + +func notifyMessage(message string) { + postLog.Info(fmt.Sprintf("%s", message)) +} + +func Init() error { + tcpConnMutex.Lock() + defer tcpConnMutex.Unlock() + + if tcpConn != nil { + return fmt.Errorf("TCP client already initialized") + } + + recvChan = make(chan string, 100) + stopRecvChan = make(chan struct{}) + isConnected = false + + return nil +} + +func tcpConnect(ipaddr string, port int) error { + tcpConnMutex.Lock() + defer tcpConnMutex.Unlock() + + if tcpConn != nil { + return fmt.Errorf("already connected") + } + + address := fmt.Sprintf("%s:%d", ipaddr, port) + conn, err := net.DialTimeout("tcp", address, 3*time.Second) + if err != nil { + return fmt.Errorf("failed to connect to %s: %v", address, err) + } + + tcpConn = conn + isConnected = true + + go recvMsg() + + return nil +} + +func sendMsg(message string, target int) (string, error) { + tcpConnMutex.Lock() + + if tcpConn == nil { + tcpConnMutex.Unlock() + return "", fmt.Errorf("not connected") + } + + _, err := tcpConn.Write([]byte(message + "\n")) + if err != nil { + tcpConnMutex.Unlock() + return "", fmt.Errorf("failed to send message: %v", err) + } + + tcpConnMutex.Unlock() + + select { + case response := <-recvChan: + return response, nil + case <-time.After(time.Duration(target) * time.Second): + return "", fmt.Errorf("timeout waiting for response") + } +} + +func recvMsg() { + reader := bufio.NewReader(tcpConn) + for { + select { + case <-stopRecvChan: + return + default: + tcpConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + line, err := reader.ReadString('\n') + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + continue + } + tcpConnMutex.Lock() + if tcpConn != nil { + tcpConn.Close() + tcpConn = nil + isConnected = false + } + tcpConnMutex.Unlock() + return + } + + line = line[:len(line)-1] + if len(line) > 0 { + select { + case recvChan <- line: + default: + select { + case <-recvChan: + recvChan <- line + default: + } + } + + if len(line) > 0 && line != "success" && !isResponseMessage(line) { + notifyMessage(line) + } + } + } + } +} + +func isResponseMessage(msg string) bool { + return msg == "success" || msg == "failed" +} + + + +func Destroy() error { + tcpConnMutex.Lock() + defer tcpConnMutex.Unlock() + + if stopRecvChan != nil { + close(stopRecvChan) + stopRecvChan = nil + } + + if tcpConn != nil { + err := tcpConn.Close() + tcpConn = nil + isConnected = false + if err != nil { + return fmt.Errorf("failed to close connection: %v", err) + } + } + + if recvChan != nil { + close(recvChan) + recvChan = nil + } + + return nil +} + +func IsConnected() bool { + tcpConnMutex.Lock() + defer tcpConnMutex.Unlock() + return isConnected && tcpConn != nil +}