package main import ( "io" "log" "os" "path/filepath" "strings" ) // setupFileLogging tees the standard logger to a file alongside the config. // Returns the log file path. Stderr is kept as a secondary sink so dev runs // (linux, console) still print, while windowsgui builds rely on the file. func setupFileLogging(cfgDir string) (string, error) { if err := os.MkdirAll(cfgDir, 0755); err != nil { return "", err } p := filepath.Join(cfgDir, "companion.log") f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return "", err } log.SetOutput(io.MultiWriter(f, os.Stderr)) return p, nil } // tailLog returns the last n lines of the log file. Reads only the trailing // chunk of the file so the cost is bounded regardless of log size. func tailLog(p string, n int) ([]string, error) { const tailBytes = 32 * 1024 f, err := os.Open(p) if err != nil { return nil, err } defer f.Close() info, err := f.Stat() if err != nil { return nil, err } size := info.Size() var off int64 if size > tailBytes { off = size - tailBytes } buf := make([]byte, size-off) if _, err := f.ReadAt(buf, off); err != nil && err != io.EOF { return nil, err } s := string(buf) // Drop the (likely partial) first line when we didn't start at 0. if off > 0 { if i := strings.IndexByte(s, '\n'); i >= 0 { s = s[i+1:] } } s = strings.TrimRight(s, "\n") if s == "" { return nil, nil } lines := strings.Split(s, "\n") if len(lines) > n { lines = lines[len(lines)-n:] } return lines, nil }