## Summary Adds `tools/hangar` — a macOS desktop control panel for working on Fleet locally: branch management, `fleet serve` orchestration, log tail, dev MySQL backup/restore, `fleetctl`, GitOps, and `osquery-perf`, all in one window. Built with **Go + [Wails 3](https://v3alpha.wails.io)** — the backend is plain Go (`os/exec`, `syscall`, goroutines) so Fleet engineers can contribute to it; only the desktop shell is Wails. The `internal/` packages are pure and unit-tested. ### History note Hangar started as a Rust/Tauri app. It was ported to Go, and **the Go port is now the canonical `tools/hangar`**. The original Rust/Tauri implementation has been removed from the monorepo (preserved in a standalone repo) — so although this branch's earlier commits add and then replace the Rust app, the net diff is just the Go app at `tools/hangar`. The bundle identifier is `com.fleetdm.fleet-hangar`, matching the original app so existing settings carry over. # Checklist for submitter - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops > No `changes/` file: `tools/` is contributor tooling, not a user-visible Fleet change. No DB migrations, no Fleet config settings, no fleetd/orbit changes. ## Testing - [x] Added/updated automated tests (Go unit tests across `internal/`, including a path-traversal regression for backup deletion) - [x] QA'd all new/changed functionality manually ## Test plan - [x] `cd tools/hangar && task dev` launches the app (live-reload) - [x] `task build` produces `bin/fleet-hangar`; `go test ./...` is green - [x] First-run gate discovers a local Fleet clone and runs dep checks - [x] Server tab can run the build chain and start `fleet serve` - [x] Git tab branch search finds an older branch (e.g. a stale `qa-*`) by name <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced Fleet Hangar, a comprehensive desktop application for Fleet development workflows, providing unified controls for server/database management, git operations, configuration, logging, and troubleshooting. * Added database backup management with metadata tracking. * Integrated process orchestration for development services (Docker, ngrok, Python). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: George Karr <georgekarrv@users.noreply.github.com>
92 lines
2.2 KiB
Go
92 lines
2.2 KiB
Go
package processes
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// logFileMaxBytes is the on-disk rotation threshold. fleet serve under debug
|
|
// logging can produce tens of MB/hour; without rotation the file grows
|
|
// unbounded. One previous generation is kept as <channel>.log.1.
|
|
const logFileMaxBytes int64 = 16 * 1024 * 1024
|
|
|
|
// logBufSize matches Rust's BufWriter default (~8KiB) — a win for chatty
|
|
// stdout while keeping per-line syscall churn down.
|
|
const logBufSize = 8192
|
|
|
|
func logFilePath(logDir, channel string) string {
|
|
return filepath.Join(logDir, channel+".log")
|
|
}
|
|
|
|
// channelWriter is a cached buffered writer for one channel's log file, with
|
|
// in-process size tracking for rotation.
|
|
type channelWriter struct {
|
|
w *bufio.Writer
|
|
file *os.File
|
|
bytes int64
|
|
path string
|
|
maxBytes int64
|
|
}
|
|
|
|
func openChannelWriter(path string, maxBytes int64) (*channelWriter, error) {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var size int64
|
|
if fi, err := f.Stat(); err == nil {
|
|
size = fi.Size()
|
|
}
|
|
return &channelWriter{
|
|
w: bufio.NewWriterSize(f, logBufSize),
|
|
file: f,
|
|
bytes: size,
|
|
path: path,
|
|
maxBytes: maxBytes,
|
|
}, nil
|
|
}
|
|
|
|
// rotateIfNeeded rotates <path> to <path>.1 once it crosses maxBytes.
|
|
func (cw *channelWriter) rotateIfNeeded() {
|
|
if cw.bytes < cw.maxBytes {
|
|
return
|
|
}
|
|
cw.w.Flush()
|
|
cw.file.Close()
|
|
_ = os.Rename(cw.path, cw.path+".1")
|
|
if nw, err := openChannelWriter(cw.path, cw.maxBytes); err == nil {
|
|
*cw = *nw
|
|
}
|
|
}
|
|
|
|
// write appends one tab-delimited record (ts, stream, message). The message
|
|
// is secret-scrubbed and has embedded tabs replaced so the format stays
|
|
// parseable. stderr is flushed immediately so crash tails are durable.
|
|
func (cw *channelWriter) write(tsMS uint64, stream, message string) {
|
|
cw.rotateIfNeeded()
|
|
msg := strings.ReplaceAll(scrubSecrets(message), "\t", " ")
|
|
line := fmt.Sprintf("%d\t%s\t%s\n", tsMS, stream, msg)
|
|
if n, err := cw.w.WriteString(line); err == nil {
|
|
cw.bytes += int64(n)
|
|
}
|
|
if stream == "stderr" {
|
|
cw.w.Flush()
|
|
}
|
|
}
|
|
|
|
func (cw *channelWriter) flush() {
|
|
if cw.w != nil {
|
|
cw.w.Flush()
|
|
}
|
|
}
|
|
|
|
func (cw *channelWriter) close() {
|
|
cw.flush()
|
|
if cw.file != nil {
|
|
cw.file.Close()
|
|
}
|
|
}
|