package frpLogger import ( "bufio" "fmt" "io" "log" "os" "os/exec" "path/filepath" "strings" "time" ) func (s *InstanceLogStreamer) streamInitDLogs() { s.sendInfo(fmt.Sprintf("Starting to stream logs for init.d service: %s", s.serviceName)) instance, err := DBQueryFrpcInstanceByID(s.instanceID) if err != nil { s.sendError(fmt.Sprintf("Failed to get instance info: %v", err)) return } startTime := getInitDServiceStartTime(s.serviceName) logFilePath := s.getLogFilePathFromConfig(instance.ConfigPath) if logFilePath != "" { if _, err := os.Stat(logFilePath); err == nil { if !startTime.IsZero() { s.streamLogFileSince(logFilePath, startTime) } else { s.streamLogFile(logFilePath) } return } } possibleLogPaths := s.getPossibleInitDLogPaths() for _, logPath := range possibleLogPaths { if _, err := os.Stat(logPath); err == nil { s.sendInfo(fmt.Sprintf("Found log file: %s", logPath)) if !startTime.IsZero() { s.streamLogFileSince(logPath, startTime) } else { s.streamLogFile(logPath) } return } } s.sendInfo("No log file found, streaming service status instead...") s.streamInitDStatus() } func (s *InstanceLogStreamer) getPossibleInitDLogPaths() []string { var paths []string paths = append(paths, fmt.Sprintf("/var/log/%s.log", s.serviceName)) paths = append(paths, fmt.Sprintf("/var/log/superfrpc/%s.log", s.serviceName)) paths = append(paths, "/var/log/frpc.log") paths = append(paths, "/var/log/superfrpc/frpc.log") instance, err := DBQueryFrpcInstanceByID(s.instanceID) if err == nil { configDir := filepath.Dir(instance.ConfigPath) paths = append(paths, filepath.Join(configDir, s.serviceName+".log")) paths = append(paths, filepath.Join(configDir, "frpc.log")) } pidFile := fmt.Sprintf("/var/run/%s.pid", s.serviceName) if pid, err := os.ReadFile(pidFile); err == nil { pidStr := strings.TrimSpace(string(pid)) paths = append(paths, fmt.Sprintf("/proc/%s/fd/1", pidStr)) paths = append(paths, fmt.Sprintf("/proc/%s/fd/2", pidStr)) } return paths } func (s *InstanceLogStreamer) streamInitDStatus() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() lastStatus := "" for { select { case <-s.stopChan: s.sendInfo("Service status monitoring stopped") return case <-ticker.C: status := s.getInitDServiceStatus() if status != lastStatus { s.sendInfo(fmt.Sprintf("Service status: %s", status)) lastStatus = status } } } } func (s *InstanceLogStreamer) getInitDServiceStatus() string { servicePath := fmt.Sprintf("/etc/init.d/%s", s.serviceName) if _, err := os.Stat(servicePath); os.IsNotExist(err) { return "NOT_INSTALLED" } cmd := exec.Command(servicePath, "status") output, err := cmd.CombinedOutput() outputStr := strings.TrimSpace(string(output)) if err != nil { if strings.Contains(outputStr, "is running") { return "RUNNING" } return "STOPPED" } if strings.Contains(outputStr, "is running") { return "RUNNING" } return "STOPPED" } func getInitDServiceLogs(serviceName string, lines int) ([]string, error) { startTime := getInitDServiceStartTime(serviceName) possibleLogPaths := []string{ fmt.Sprintf("/var/log/%s.log", serviceName), fmt.Sprintf("/var/log/superfrpc/%s.log", serviceName), "/var/log/frpc.log", } for _, logPath := range possibleLogPaths { if _, err := os.Stat(logPath); err == nil { if !startTime.IsZero() { return readLinesSinceTime(logPath, lines, startTime) } return readLastLines(logPath, lines) } } return nil, fmt.Errorf("no log file found for service %s", serviceName) } func readLinesSinceTime(filePath string, maxLines int, sinceTime time.Time) ([]string, error) { file, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("failed to open log file: %w", err) } defer file.Close() var result []string scanner := bufio.NewScanner(file) allLines := make([]string, 0) for scanner.Scan() { line := scanner.Text() allLines = append(allLines, line) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("error reading log file: %w", err) } for i := len(allLines) - 1; i >= 0 && len(result) < maxLines; i-- { line := allLines[i] lineTime := parseLogLineTime(line) if lineTime.IsZero() || lineTime.After(sinceTime) || lineTime.Equal(sinceTime) { result = append([]string{line}, result...) } } return result, nil } func parseLogLineTime(line string) time.Time { layouts := []string{ "2006/01/02 15:04:05", "2006-01-02 15:04:05", "Jan 2 15:04:05", "Jan 02 15:04:05", "02/Jan/2006:15:04:05", time.RFC3339, } for _, layout := range layouts { if len(line) >= len(layout) { t, err := time.Parse(layout, strings.TrimSpace(line[:min(len(line), len(layout))])) if err == nil { return t } } } return time.Time{} } func min(a, b int) int { if a < b { return a } return b } func readLastLines(filePath string, lines int) ([]string, error) { file, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("failed to open log file: %w", err) } defer file.Close() var result []string scanner := bufio.NewScanner(file) allLines := make([]string, 0) for scanner.Scan() { allLines = append(allLines, scanner.Text()) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("error reading log file: %w", err) } start := 0 if len(allLines) > lines { start = len(allLines) - lines } result = allLines[start:] return result, nil } func isServiceRunningInitD(serviceName string) bool { servicePath := fmt.Sprintf("/etc/init.d/%s", serviceName) if _, err := os.Stat(servicePath); os.IsNotExist(err) { return false } cmd := exec.Command(servicePath, "status") output, err := cmd.CombinedOutput() if err != nil { return false } return strings.Contains(string(output), "is running") } func getInitDServicePid(serviceName string) int { pidFile := fmt.Sprintf("/var/run/%s.pid", serviceName) if content, err := os.ReadFile(pidFile); err == nil { pidStr := strings.TrimSpace(string(content)) var pid int if _, err := fmt.Sscanf(pidStr, "%d", &pid); err == nil { return pid } } return 0 } func getProcessStartTime(pid int) time.Time { if pid <= 0 { return time.Time{} } statPath := fmt.Sprintf("/proc/%d/stat", pid) content, err := os.ReadFile(statPath) if err != nil { return time.Time{} } fields := strings.Fields(string(content)) if len(fields) < 22 { return time.Time{} } starttimeTicks, err := fmt.Sscanf(fields[21], "%d", new(uint64)) if err != nil || starttimeTicks != 1 { return time.Time{} } var ticks uint64 fmt.Sscanf(fields[21], "%d", &ticks) clkTck := uint64(100) uptimeBytes, err := os.ReadFile("/proc/uptime") if err != nil { return time.Time{} } uptimeParts := strings.Fields(string(uptimeBytes)) if len(uptimeParts) < 1 { return time.Time{} } var uptimeSeconds float64 fmt.Sscanf(uptimeParts[0], "%f", &uptimeSeconds) secondsSinceBoot := float64(ticks) / float64(clkTck) processStartTime := time.Now().Add(-time.Duration(uptimeSeconds-secondsSinceBoot) * time.Second) return processStartTime } func getInitDServiceStartTime(serviceName string) time.Time { pid := getInitDServicePid(serviceName) if pid <= 0 { return time.Time{} } return getProcessStartTime(pid) } func (s *InstanceLogStreamer) streamInitDLogsFromProc() { pid := getInitDServicePid(s.serviceName) if pid == 0 { s.sendError("Failed to get service PID") return } stdoutPath := fmt.Sprintf("/proc/%d/fd/1", pid) stderrPath := fmt.Sprintf("/proc/%d/fd/2", pid) s.sendInfo(fmt.Sprintf("Attempting to stream from /proc/%d", pid)) go s.streamProcFile(stdoutPath) go s.streamProcFile(stderrPath) } func (s *InstanceLogStreamer) streamProcFile(path string) { file, err := os.Open(path) if err != nil { s.sendError(fmt.Sprintf("Failed to open %s: %v", path, err)) return } defer file.Close() reader := bufio.NewReader(file) for { select { case <-s.stopChan: return default: line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { time.Sleep(100 * time.Millisecond) continue } return } line = strings.TrimSpace(line) if line != "" { s.sendLog(s.parseLogLevel(line), line) } } } } func (s *InstanceLogStreamer) streamInitDLogsViaSyslog() { s.sendInfo("Attempting to stream logs via syslog...") cmd := exec.Command("tail", "-f", "/var/log/syslog") stdout, err := cmd.StdoutPipe() if err != nil { s.sendError(fmt.Sprintf("Failed to create stdout pipe: %v", err)) return } if err := cmd.Start(); err != nil { s.sendError(fmt.Sprintf("Failed to start tail: %v", err)) return } go func() { <-s.stopChan cmd.Process.Kill() cmd.Wait() }() reader := bufio.NewReader(stdout) for { select { case <-s.stopChan: cmd.Process.Kill() cmd.Wait() return default: line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { time.Sleep(100 * time.Millisecond) continue } return } line = strings.TrimSpace(line) if line != "" && strings.Contains(line, s.serviceName) { s.sendLog(s.parseLogLevel(line), line) } } } } func init() { log.Printf("[frpLogger] Init.d log streamer initialized") }