From f6005cb32496d6d28c9fb9d449fa05c0730889a3 Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Thu, 2 Apr 2026 21:56:40 +0800 Subject: [PATCH] feat: add initial project structure with logging and watchdog service Implement basic watchdog service for Linux systemd with: - Configuration loading - Logging system with database support - WebSocket log broadcasting - TCP server for agent communication - Project setup with Go modules --- .gitignore | 49 +++++++++++++++++++ config.go | 29 +++++++++++ config.json | 3 ++ go.mod | 5 ++ go.sum | 2 + main.go | 78 ++++++++++++++++++++++++++++++ postLog/database.go | 60 +++++++++++++++++++++++ postLog/logBroadcaster.go | 96 +++++++++++++++++++++++++++++++++++++ postLog/logSocketHandler.go | 66 +++++++++++++++++++++++++ postLog/postLog.go | 95 ++++++++++++++++++++++++++++++++++++ 10 files changed, 483 insertions(+) create mode 100644 .gitignore create mode 100644 config.go create mode 100644 config.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 postLog/database.go create mode 100644 postLog/logBroadcaster.go create mode 100644 postLog/logSocketHandler.go create mode 100644 postLog/postLog.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac0a1e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# ---> VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +/frp_client +/configs +agent.md +super-frpc +super-frpc.exe +*.db +database.db +logs.html \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..921fbea --- /dev/null +++ b/config.go @@ -0,0 +1,29 @@ +package main + + +import ( + "Watchdog_Linux-systemd/postLog" + "encoding/json" + "fmt" + "os" +) + +var Config struct { + DebugMode bool `json:"debugMode"` +} + +func loadConfig() { + configFile, err := os.Open("config.json") + if err != nil { + postLog.Fatal(fmt.Sprintf("Failed to open config file: %v, err: %v", configFile, err)) + } + defer configFile.Close() + decoder := json.NewDecoder(configFile) + if err := decoder.Decode(&Config); err != nil { + postLog.Fatal(fmt.Sprintf("Failed to decode config file: %v, err: %v", configFile, err)) + } + + if Config.DebugMode { + isDebug = true + } +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..1ecaa7d --- /dev/null +++ b/config.json @@ -0,0 +1,3 @@ +{ + "debugMode": true +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e54b75f --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module Watchdog_Linux-systemd + +go 1.24.0 + +require github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..25a9fc4 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ea8bf0d --- /dev/null +++ b/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "Watchdog_Linux-systemd/postLog" + "fmt" + "net" +) + +const ( + listenAddr = "127.0.0.1" + listenPort = "10080" + Type = "tcp" +) + +type SoftwareInfo struct { + Name string + Version string + Developer string + BuildVer int16 + Description string + BuildType string +} + +var softwareInfo SoftwareInfo = SoftwareInfo{ + Name: "Watchdog_Linux-systemd", + Version: "0.0.1", + Developer: "Super-frpc", + BuildVer: 1, + Description: "Super-frpc Watchdog service for Linux systemd", + BuildType: "Debug", +} + +var isDebug bool + +func main() { + loadConfig() + if isDebug == true { + postLog.SetDebugMode(true) + } + postLog.Info(fmt.Sprintf("%s %s (Build %d.%s) by %s", softwareInfo.Name, softwareInfo.Version, softwareInfo.BuildVer, softwareInfo.BuildType, softwareInfo.Developer)) + listen, err := net.Listen(Type, fmt.Sprintf("%s:%s", listenAddr, listenPort)) + if err != nil { + postLog.Fatal(fmt.Sprintf("Failed to listen: %v, err: %v, %v", listenAddr, listenPort, err)) + } + defer listen.Close() + postLog.Info(fmt.Sprintf("Server is running on %s:%s", listenAddr, listenPort)) + + for { + conn, err := listen.Accept() + if err != nil { + postLog.Error(fmt.Sprintf("Failed to accept: %v, err: %v", conn, err)) + } + go handleRequest(conn) + } +} + +func handleRequest(conn net.Conn) { + defer conn.Close() + + buffer := make([]byte, 1024) + n, err := conn.Read(buffer) + if err != nil { + postLog.Error(fmt.Sprintf("Failed to read: %v, err: %v", conn, err)) + return + } + + recvMsg := string(buffer[:n]) + postLog.Debug(fmt.Sprintf("Received: %v", recvMsg)) + responseMsg := "" + if recvMsg == "watchdogAgentConnectionTest" { + responseMsg = "success" + } else { + responseMsg = "error: unknown message" + } + if _, err := conn.Write([]byte(responseMsg)); err != nil { + postLog.Error(fmt.Sprintf("Failed to write: %v, err: %v", conn, err)) + } +} diff --git a/postLog/database.go b/postLog/database.go new file mode 100644 index 0000000..8b14db4 --- /dev/null +++ b/postLog/database.go @@ -0,0 +1,60 @@ +package postLog + +import ( + "database/sql" + "fmt" + "os" + "time" +) + +var ( + logsDB *sql.DB + tableName string +) + +func InitLogsDatabase(dbPath string) error { + var err error + logsDB, err = sql.Open("sqlite", dbPath) + if err != nil { + return fmt.Errorf("failed to open logs database: %w", err) + } + + if err = logsDB.Ping(); err != nil { + return fmt.Errorf("failed to ping logs database: %w", err) + } + + timestamp := time.Now().Format("20060102_150405") + tableName = fmt.Sprintf("logs_%s", timestamp) + + createTableSQL := fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level INTEGER NOT NULL, + content TEXT NOT NULL, + timestamp TEXT NOT NULL DEFAULT (datetime('now')) + );`, tableName) + + _, err = logsDB.Exec(createTableSQL) + if err != nil { + return fmt.Errorf("failed to create logs table %s: %w", tableName, err) + } + + return nil +} + +func insertLogToDB(db *sql.DB, level int, content string, timestamp string) { + if db != nil { + _, err := db.Exec(fmt.Sprintf(`INSERT INTO %s (level, content, timestamp) VALUES (%d, '%s', '%s')`, tableName, + level, content, timestamp)) + // fmt.Fprintf(os.Stderr, `INSERT INTO %s (level, content, timestamp) VALUES (%d, '%s', '%s')`, tableName, level, content, timestamp) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to write log to database: %v\n", err) + } + } +} + +func SetLogsDatabase(db *sql.DB) { + loggerMutex.Lock() + defer loggerMutex.Unlock() + logsDB = db +} diff --git a/postLog/logBroadcaster.go b/postLog/logBroadcaster.go new file mode 100644 index 0000000..3879792 --- /dev/null +++ b/postLog/logBroadcaster.go @@ -0,0 +1,96 @@ +package postLog + +import ( + "sync" +) + +type LogMessage struct { + Level int `json:"level"` + Content string `json:"content"` + Timestamp string `json:"timestamp"` +} + +type Client struct { + ID string + Messages chan LogMessage +} + +type LogBroadcaster struct { + mu sync.RWMutex + clients map[string]chan LogMessage + history []LogMessage + historyM sync.RWMutex +} + +var broadcaster *LogBroadcaster + +func InitLogBroadcaster() { + broadcaster = &LogBroadcaster{ + clients: make(map[string]chan LogMessage), + history: make([]LogMessage, 0), + } +} + +func GetLogBroadcaster() *LogBroadcaster { + return broadcaster +} + +func (lb *LogBroadcaster) AddClient(id string) chan LogMessage { + lb.mu.Lock() + defer lb.mu.Unlock() + + ch := make(chan LogMessage, 100) + lb.clients[id] = ch + return ch +} + +func (lb *LogBroadcaster) RemoveClient(id string) { + lb.mu.Lock() + defer lb.mu.Unlock() + + if ch, exists := lb.clients[id]; exists { + close(ch) + delete(lb.clients, id) + } +} + +func (lb *LogBroadcaster) Broadcast(msg LogMessage) { + lb.mu.RLock() + defer lb.mu.RUnlock() + + for _, ch := range lb.clients { + select { + case ch <- msg: + default: + } + } +} + +func (lb *LogBroadcaster) GetHistory() []LogMessage { + lb.historyM.RLock() + defer lb.historyM.RUnlock() + + result := make([]LogMessage, len(lb.history)) + copy(result, lb.history) + return result +} + +func (lb *LogBroadcaster) AddToHistory(msg LogMessage) { + lb.historyM.Lock() + defer lb.historyM.Unlock() + + lb.history = append(lb.history, msg) + if len(lb.history) > 100 { + lb.history = lb.history[1:] + } +} + +func (lb *LogBroadcaster) SendHistory(clientCh chan LogMessage) { + history := lb.GetHistory() + for _, msg := range history { + select { + case clientCh <- msg: + default: + } + } +} diff --git a/postLog/logSocketHandler.go b/postLog/logSocketHandler.go new file mode 100644 index 0000000..3981613 --- /dev/null +++ b/postLog/logSocketHandler.go @@ -0,0 +1,66 @@ +package postLog + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +type LogSocketHandler struct { + broadcaster *LogBroadcaster +} + +func NewLogSocketHandler(b *LogBroadcaster) *LogSocketHandler { + return &LogSocketHandler{ + broadcaster: b, + } +} + +func (h *LogSocketHandler) Handle(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Failed to upgrade connection: %v", err) + return + } + defer conn.Close() + + clientID := conn.RemoteAddr().String() + "-" + time.Now().Format("20060102150405") + clientCh := h.broadcaster.AddClient(clientID) + defer h.broadcaster.RemoveClient(clientID) + + h.broadcaster.SendHistory(clientCh) + + done := make(chan struct{}) + go func() { + defer close(done) + for { + msg := <-clientCh + data, err := json.Marshal(msg) + if err != nil { + log.Printf("Failed to marshal log message: %v", err) + return + } + if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { + return + } + } + }() + + for { + _, _, err := conn.ReadMessage() + if err != nil { + return + } + } +} diff --git a/postLog/postLog.go b/postLog/postLog.go new file mode 100644 index 0000000..364eef8 --- /dev/null +++ b/postLog/postLog.go @@ -0,0 +1,95 @@ +package postLog + +import ( + "fmt" + "sync" + "time" +) + +var ( + loggerMutex sync.Mutex + isDebug bool +) + +const ( + DEBUG = iota + INFO + WARNING + ERROR + FATAL +) + +var levelNames = []string{"DEBUG", "INFO", "WARNING", "ERROR", "FATAL"} +var levelColors = []int{34, 27, 220, 196, 124} + +func colorOut_256(s string, foreColor int) string { + return fmt.Sprintf("\033[38;5;%dm%s\033[m", foreColor, s) +} + +func getSystemTime() string { + now := time.Now() + return now.Format("2006-01-02 15:04:05.000") +} + +func SetDebugMode(debug bool) { + loggerMutex.Lock() + defer loggerMutex.Unlock() + isDebug = debug +} + +func PostLog(message string, level int) { + timeNow := getSystemTime() + + idx := level + if idx < 0 || idx >= len(levelNames) { + idx = INFO + } + + loggerMutex.Lock() + + if idx == DEBUG && !isDebug { + loggerMutex.Unlock() + return + } + + levelDisplay := colorOut_256(levelNames[idx], levelColors[idx]) + db := logsDB + + loggerMutex.Unlock() + + fmt.Printf("[%s - %s] %s\n", timeNow, levelDisplay, message) + insertLogToDB(db, level, message, timeNow) + + if broadcaster != nil { + broadcaster.AddToHistory(LogMessage{ + Level: level, + Content: message, + Timestamp: timeNow, + }) + broadcaster.Broadcast(LogMessage{ + Level: level, + Content: message, + Timestamp: timeNow, + }) + } +} + +func Debug(message string) { + PostLog(message, DEBUG) +} + +func Info(message string) { + PostLog(message, INFO) +} + +func Warning(message string) { + PostLog(message, WARNING) +} + +func Error(message string) { + PostLog(message, ERROR) +} + +func Fatal(message string) { + PostLog(message, FATAL) +}