feat(watchdog): add watchdog service with TCP client implementation

- Implement TCP client for watchdog service communication
- Add watchdog configuration in config.json
- Update main.go to initialize and connect to watchdog
- Add watchdog related functions (connect, command handling)
- Update README.md with new watchdog feature
- Improve route setup logging in router.go
This commit is contained in:
2026-04-02 18:42:44 +08:00
parent c19bbfbca4
commit aa22b04a1f
7 changed files with 272 additions and 2 deletions

View File

@@ -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

View File

@@ -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)
}

12
main.go
View File

@@ -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,

View File

@@ -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) {

33
watchdog/command.go Normal file
View File

@@ -0,0 +1,33 @@
package watchdog
import (
"fmt"
)
func AddInstance(serviceName string) bool {
if !IsConnected() {
return false
}
message := fmt.Sprintf("[addInstance] <serviceName>%s</serviceName>", 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] <serviceName>%s</serviceName>", serviceName)
response, err := sendMsg(message, 3)
if err != nil {
return false
}
return response == "success"
}

47
watchdog/connect.go Normal file
View File

@@ -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
}

164
watchdog/tcpClient.go Normal file
View File

@@ -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
}