package main import ( "archive/tar" "bufio" "compress/gzip" "errors" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "regexp" "strings" "time" ) var safeFilenamePattern = regexp.MustCompile(`[^A-Za-z0-9_.-]`) func (app *App) log_line(line string) { handle, err := os.OpenFile(app.paths.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return } defer handle.Close() _, _ = fmt.Fprintln(handle, line) } func load_logging_pref(path string) bool { file, err := os.Open(path) if err != nil { return false } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, value, found := strings.Cut(line, "=") if !found || !strings.EqualFold(strings.TrimSpace(key), "saveSessionLogs") { continue } switch strings.ToLower(strings.TrimSpace(value)) { case "1", "true", "yes", "on": return true default: return false } } return false } func save_logging_pref(path string, enabled bool) error { return write_private_file(path, func(writer io.Writer) error { value := 0 if enabled { value = 1 } _, err := fmt.Fprintf(writer, "saveSessionLogs=%d\n", value) return err }) } func sanitize_filename(value string) string { return safeFilenamePattern.ReplaceAllString(value, "_") } func prepare_session_log(logDir, serverName string) (string, error) { if err := os.MkdirAll(logDir, 0700); err != nil { return "", err } path := filepath.Join(logDir, fmt.Sprintf("%s-%s.log", sanitize_filename(serverName), time.Now().Format("2006-01-02"))) handle, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return "", err } if err := handle.Chmod(0600); err != nil { handle.Close() return "", err } if err := handle.Close(); err != nil { return "", err } return path, nil } func log_files(logDir string) ([]string, error) { entries, err := os.ReadDir(logDir) if err != nil { return nil, err } paths := []string{} for _, entry := range entries { if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), ".log") { paths = append(paths, filepath.Join(logDir, entry.Name())) } } return paths, nil } func create_log_archive(logDir, cacheDir string) (string, error) { paths, err := log_files(logDir) if err != nil { return "", err } if len(paths) == 0 { return "", fs.ErrNotExist } if err := os.MkdirAll(cacheDir, 0700); err != nil { return "", err } archiveFile, err := os.CreateTemp(cacheDir, "barnard-logs-*.tar.gz") if err != nil { return "", err } archivePath := archiveFile.Name() failed := true defer func() { if failed { _ = os.Remove(archivePath) } }() if err := archiveFile.Chmod(0600); err != nil { archiveFile.Close() return "", err } gzipWriter := gzip.NewWriter(archiveFile) tarWriter := tar.NewWriter(gzipWriter) for _, path := range paths { info, err := os.Stat(path) if err != nil { tarWriter.Close() gzipWriter.Close() archiveFile.Close() return "", err } header, err := tar.FileInfoHeader(info, "") if err != nil { tarWriter.Close() gzipWriter.Close() archiveFile.Close() return "", err } header.Name = filepath.Base(path) header.Mode = 0600 if err := tarWriter.WriteHeader(header); err != nil { tarWriter.Close() gzipWriter.Close() archiveFile.Close() return "", err } file, err := os.Open(path) if err != nil { tarWriter.Close() gzipWriter.Close() archiveFile.Close() return "", err } _, copyErr := io.Copy(tarWriter, file) closeErr := file.Close() if copyErr != nil { tarWriter.Close() gzipWriter.Close() archiveFile.Close() return "", copyErr } if closeErr != nil { tarWriter.Close() gzipWriter.Close() archiveFile.Close() return "", closeErr } } if err := tarWriter.Close(); err != nil { gzipWriter.Close() archiveFile.Close() return "", err } if err := gzipWriter.Close(); err != nil { archiveFile.Close() return "", err } if err := archiveFile.Sync(); err != nil { archiveFile.Close() return "", err } if err := archiveFile.Close(); err != nil { return "", err } failed = false return archivePath, nil } func (app *App) send_logs() error { wormhole, err := exec.LookPath("wormhole") if err != nil { return app.ui.message("Required command not found: wormhole") } archivePath, err := create_log_archive(app.paths.LogDir, app.paths.CacheDir) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, os.ErrNotExist) { return app.ui.message("No logs to send. Logs are saved to: " + app.paths.LogDir) } if err != nil { return app.ui.message("Could not create log archive: " + err.Error()) } defer os.Remove(archivePath) if err := app.ui.message("Wormhole will display the transfer code in the normal terminal. Press Ctrl+C there to cancel the transfer."); err != nil { return err } command := exec.Command(wormhole, "send", archivePath) command.Stdin = os.Stdin command.Stdout = os.Stdout command.Stderr = os.Stderr app.ui.close() commandErr := run_external(app.ui, command) var signalErr *terminalSignalError if errors.As(commandErr, &signalErr) { return signalErr } if err := app.ui.open(); err != nil { return err } if receivedSignal := app.ui.take_termination(); receivedSignal != nil { return &terminalSignalError{signal: receivedSignal} } if commandErr == nil { app.log_line("Sent log archive with wormhole") return app.ui.message("Logs sent successfully.") } return app.ui.message("Log transfer did not complete successfully: " + commandErr.Error()) } func (app *App) toggle_session_logging() error { question := "Session logging is currently disabled. Enable saving logs to the logs directory?" if app.saveSessionLogs { question = "Session logging is currently enabled. Disable it?" } confirmed, err := app.ui.confirm(question) if err != nil || !confirmed { return err } newValue := !app.saveSessionLogs if err := save_logging_pref(app.paths.LogPrefsFile, newValue); err != nil { return app.ui.message("Could not save logging preference: " + err.Error()) } app.saveSessionLogs = newValue return nil } func (app *App) manage_logs() error { for { toggleLabel := "Enable logs" if app.saveSessionLogs { toggleLabel = "Disable logs" } options := []string{toggleLabel, "Send logs with wormhole", "Go Back"} selection, cancelled, err := app.ui.menu(options) if err != nil || cancelled || selection == len(options)-1 { return err } switch selection { case 0: err = app.toggle_session_logging() case 1: err = app.send_logs() } if err != nil { return err } } }