Skip to content

v0.6.4: daemon lifecycle, clipboard, network, docs fixes - #27

Merged
bethropolis merged 36 commits into
mainfrom
dev
Jul 23, 2026
Merged

v0.6.4: daemon lifecycle, clipboard, network, docs fixes#27
bethropolis merged 36 commits into
mainfrom
dev

Conversation

@bethropolis

@bethropolis bethropolis commented Jul 23, 2026

Copy link
Copy Markdown
Owner

What's new in v0.6.4

Fixes

  • Daemon lifecycle: stop uses Signal(0) liveness polling, guard existing daemon, force auto-accept in daemon child, platform-specific stop files
  • Clipboard message handling: only short-circuit when Size matches Preview; NoClipboard fallback saves to DownloadDir; history + exec hook logging
  • Clipboard read: cmd.Output() instead of CombinedOutput(); guard whitespace-only config splits
  • ANSI injection: cli.Sanitize() applied to all remote-controlled display data
  • Exec hook placeholders: %f/%n/%s/%a/%i now properly replaced in hook string
  • Hostname resolution: --ip falls back to net.LookupIP for mDNS/hostnames
  • Peer rediscovery: IP/Port/Alias/Protocol updated on re-announce (not just LastSeen)
  • Smart subnet scanning: new GetUsableSubnetIPsFromIP respects interface netmask, caps at /22
  • Cross-OS path normalization: filepath.ToSlash on incoming filenames
  • ANSI TTY guard: term.IsTerminal check before VT100 escape sequences
  • Windows clipboard UTF-8: PowerShell Set-Clipboard for proper Unicode handling
  • Windows stop: handles FindProcess always-succeeds behavior

New features

  • Daemon mode: serve --daemon/-d forks background; localgo stop sends SIGTERM
  • Config env var overrides: LOCALSEND_SHELL, LOCALSEND_CLIPBOARD_WRITE_CMD/_READ_CMD, LOCALSEND_TLS_CERT/_KEY, LOCALSEND_NOTIFICATION_CMD
  • TLS fingerprint from custom cert: server computes SHA-256 of custom leaf certificate
  • In-memory send count: progress header counts both file and clipboard/stdin payloads
  • --version/-v: now works without a subcommand; localgo help version shows proper help

Documentation

  • README: Docker example, expanded env vars, updated command table
  • CLI_REFERENCE: new flags for all subcommands, daemon/stop/health/docs
  • CODE_WALKTHROUGH: matches current package structure
  • CONFIGURATION: env vars, precedence, all flags
  • LIBRARY_GUIDE: updated API signatures

Full diff: v0.6.3...dev

saveTextAsFile now returns error instead of void. Caller maps:
- path traversal -> 400 Bad Request
- save failure -> 500 Internal Server Error
- success -> 200 OK (explicit, not implicit)

Tests added for all three branches.
New methods on ReceiveService:
- ClaimFile: validates session/senderIP/fileId/token under mutex, marks as uploading
- CompleteFile: removes file from session after success
- FailFile: resets file state to pending on failure
- GetSessionProgress: safe RLock read of progress bar pointer

UploadHandlerV2 now uses ClaimFile/CompleteFile/FailFile instead of
GetSessionByID + RemoveFileFromSession, preventing duplicate concurrent
uploads of the same token.

Tests: concurrent claim (one success, one ErrAlreadyUploading),
error cases (invalid session/file/token/IP), CompleteFile lifecycle
Refactored stripping to write to a separate destination path:
- StripTo(src, dest) writes stripped image to dest, never touches src
- Strip(path) uses temp-in-dir + rename for atomic in-place replacement
- stripJPEGTo/stripPNGTo: fail closed when SOS/IEND not found
- PNG: strip eXIf chunk in addition to tEXt/zTXt/iTXt
- Magic sniff (checks first 8 bytes), not just file extension
- writeAtomic helper for safe temp+rename writes

SendFile private mode: strips to temp file, uploads temp, defers cleanup.
Original file bytes are never modified.

Tests: JPEG EXIF removal, original unchanged, truncated JPEG error,
PNG eXIf stripping, non-image passthrough, nonexistent file error
sanitizeName strips ASCII control bytes 0x00-0x1F from FileName
in PrepareUploadHandlerV2 after decoding the request, preventing
UI spoofing and terminal escape injection via display prompts.

Test: control-char filename in prepare-upload returns 200 (sanitized)
Guard against f.Size < 0 in PrepareUploadHandlerV2's disk-space
loop. Without this check, a negative size reduces totalSize and
bypasses the free-space guard, and the value flows into uint64
conversion which wraps to a large positive number.

Test: prepare-upload with Size: -10 returns 400 Bad Request
Release sessionMutex before calling Progress.Wait() to match the
pattern used by cleanupLoop and CloseAllSessions. This prevents
stalling other operations (like concurrent ClaimFile calls) during
the progress bar Wait, which can block on terminal rendering.
Private mode previously created empty temp files for non-image files
(PDFs, zips, text files) because StripTo returns nil for non-images
(no error, no output). The empty temp then replaced the real file in
the upload map, causing 0-byte uploads.

Fix: export IsImageFile from metadata package; guard the strip loop
so non-images keep their original path unchanged.
After sanitizing control chars from filenames, reject names that
become empty with 400 Bad Request (prevents downstream confusion).

Strengthened TestPrepareUpload_SanitizesControlChars to assert the
stored session filename is 'bad.txt' after sanitization (not just
that prepare returns 200).
…pty not error

- Use CombinedOutput() instead of Output() to capture stderr
- Include tool name in both Read() and Write() error messages
- Treat tool exit with no output as empty clipboard (xclip/wl-paste exit 1
  when clipboard is empty) instead of returning a cryptic error
- Expand 'no tool found' message with actionable install hints
Remove the temp file creation for --clipboard and --stdin flags, and
instead thread raw byte content through the send pipeline via a new
SendOption/WithInMemoryFile mechanism.

Changes:
- pkg/send/send.go: Add SendOption, WithInMemoryFile, sendConfig, memFile
  types. SendToDevice and SendFiles now accept variadic SendOption.
  Process in-memory files alongside file-based files in the fileDto
  build loop and upload goroutines.
- pkg/send/upload.go: Add memReadSeekCloser (bytes.Reader wrapper with
  no-op Close), fileReader interface. Extract uploadStream() from
  uploadFile() so both file and in-memory uploads share the same stream
  logic.
- cmd/localgo/cmd/send.go: Replace os.CreateTemp/defer os.Remove with
  send.WithInMemoryFile() calls. Remove the localgo-clip- prefix hack
  entirely. Adjust file picker/empty checks to account for sendOpts.
Evaluate session.SessionID and file token before goroutines to
avoid concurrent map access without the session mutex.
…cations

Adds 6 new env var overrides:
- LOCALSEND_SHELL — custom shell for exec hooks (replaces hardcoded sh -c/cmd /c)
- LOCALSEND_CLIPBOARD_WRITE_CMD / _READ_CMD — custom clipboard tools
- LOCALSEND_TLS_CERT / LOCALSEND_TLS_KEY — custom TLS certificate paths
- LOCALSEND_NOTIFICATION_CMD — custom notification command
Adds cli.Sanitize() as a central ANSI-strip helper and applies it at all
terminal output points for remote-controlled data (aliases, filenames):
- CLI device table/quiet/JSON output and PickDevice TUI
- Incoming transfer prompts (file + clipboard)
- Server log messages
- sender model.DeviceInfo before prompt rendering
GetInterfaceIPNet returns the IPv4 network (IP+mask) for a named interface.
GetUsableSubnetIPs returns all usable host IPs respecting the actual netmask,
capped at /22 for practical LAN scanning.
serve --daemon forks into background, writes PID to
~/.config/localgo/localgo.pid, and detaches from the terminal.
localgo stop reads the PID file and sends SIGTERM (with 5s
graceful timeout, then SIGKILL).
…in in tests

Fixes #26: strings.TrimSuffix(configPath, "/config.yaml") uses hardcoded
forward slash which doesn't match Windows backslash paths. Replaced with
filepath.Dir(configPath) for cross-platform parent directory extraction.

Also fixes storage_test.go to use filepath.Join instead of manual string
concatenation with forward slashes.
syscall.SysProcAttr.Setpgid is Unix-only, causing Windows CI failure.
daemonize() is now in daemon_unix.go (Unix: fork with Setpgid) and
daemon_windows.go (Windows: returns error about unsupported daemon mode).
os.Chmod(tempDir, 0500) doesn't make directories read-only on Windows
(unlike Unix permission bits), so the test was expecting a 500 error but
getting 200.
…itization

- Only short-circuit clipboard when single file with Size matching Preview
  length (avoids misclassifying multi-file transfers)
- When NoClipboard or clipboard.Write fails, save text to DownloadDir
  instead of discarding it
- Log clipboard transfers to history and run exec hook on accept
- Sanitize clipboard preview text before rendering in confirmation prompt
…ig values

- Read() uses cmd.Output() (stdout only) instead of CombinedOutput()
  so stderr diagnostics aren't mixed into clipboard text on success
- OverrideProvider guards wp[0]/rp[0] access after strings.Fields
  with length check — whitespace-only input no longer panics
- exec.go Shell parsing similarly guarded against empty fields
- Changed hostBits > 22 to hostBits > 10 so /16 (16 host bits) and
  other large subnets are capped at /22 (max 1022 usable hosts)
- Fixed error message from 'too large' to 'too small' for tiny
  prefixes (/31, /32) that have fewer than 2 usable hosts
When CustomTLSCertPath/CustomTLSKeyPath are set, the server now parses
the leaf certificate, computes its SHA-256 fingerprint, and stores it
via Config.SetCustomFingerprint(). GetFingerprint() prefers this over
the auto-generated SecurityContext hash, so advertised fingerprints
match the actual presented certificate for trust verification.
- Use len(files) + len(sendOpts) instead of len(files) for total count
- Display clipboard/stdin entries as '(in-memory)' in the file listing
…esource leak

- Extract pidFilePath() into shared pid.go for reuse
- Add defer os.Remove(pidPath) in daemon child path (serve.go)
- Split stop.go into stop_unix.go (POSIX signals + 5s poll) and
  stop_windows.go (Kill-based) to fix platform incompatibility
- Use storage.ResolveDuplicateFilename for clipboard fallback saves
  to prevent silent overwrite; restrict to 0600 permissions
- defer body.Close() after NewIdleTimeoutReader to ensure timer is
  stopped on early-return error paths (upload.go)
…dle Windows FindProcess behavior

- Replace %f/%n/%s/%a/%i placeholders in ExecHook before passing to
  the shell (prevents literal %n etc. from being passed as-is).
- stop_windows.go: os.FindProcess always returns nil err on Windows.
  Remove dead error branch; print clean warning when Kill fails.
When --ip receives a hostname (e.g. myphone.local, desktop-pc) instead
of a raw IP, fall back to net.LookupIP for resolution. Preserves the
existing raw-IP fast path.
When a known device re-announces with different metadata (DHCP
renewal, alias change), update the existing map entry instead of
only refreshing LastSeen. The peer cache was already receiving the
new device object.
Add GetUsableSubnetIPsFromIP which finds the interface owning a given
IP and returns its actual subnet via GetUsableSubnetIPs (capped at /22).
Falls back to the legacy /24 scan when the interface cannot be determined.

Replaced all 4 call sites (scan.go, send.go, discover.go, pkg/send/send.go)
that were hardcoding /24 via GetSubnetIPs.
…hs, fix Windows clipboard UTF-8

- progress.go: guard \033[F\033[K clearing with term.IsTerminal() to avoid
  raw escape sequences in non-TTY stderr (Docker, shell redirects)
- receive_upload.go: normalize incoming filenames with filepath.ToSlash()
  so Windows backslashes form correct subdirectories on Unix receivers
- clipboard_windows.go: switch write from clip.exe to PowerShell
  Set-Clipboard via stdin pipeline for proper Unicode/UTF-8 handling
- Add golang.org/x/term as direct dependency for portable TTY detection
- Register 'config' in help.ShowMainUsage() so it appears in COMMANDS
- Add 'config' help entry in commands.go for localgo help config
- Add SetHelpFunc + import in config.go for proper help display
- Add Run to rootCmd so PersistentPreRunE fires for 'localgo --version'
  (without a subcommand), enabling the version flag check
- Add -v shorthand for --version flag
- Add version entry to commands.go so 'localgo help version' works
- Add SetHelpFunc to versionCmd for proper help display
@bethropolis bethropolis changed the title v0.6.4: REVIEW.md fixes, daemon lifecycle, clipboard, network, docs v0.6.4: daemon lifecycle, clipboard, network, docs fixes Jul 23, 2026
@bethropolis
bethropolis requested a review from Copilot July 23, 2026 08:14
…ting in-place

The previous code modified existingDevice in-place (line 148 wrote
existingDevice.Alias = device.Alias). When a second multicast listener
received the same packet (multi-interface listening), existingDevice was
the same pointer stored by the first call—already sent to a handler
goroutine and potentially being read by the test. Replacing the map entry
with the new pointer avoids sharing the old pointer across goroutines.

FromMulticastDto already sets LastSeen: time.Now(), so the incoming
device has a fresh timestamp—no need for UpdateLastSeen().

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Release-focused update for v0.6.4 that extends LocalGo’s CLI/server lifecycle (daemon + stop/config/version), tightens receiver upload/clipboard handling and sanitization, and improves discovery/scanning and private-mode behavior, alongside broad documentation refresh.

Changes:

  • Adds daemonized serve mode plus stop, config, and improved version behavior/help.
  • Refactors receiver upload/session handling (atomic “claim”, retryable failures), improves clipboard-message flow, and expands ANSI/control-character sanitization.
  • Improves sender/discovery behavior (usable-subnet scanning, peer rediscovery updates) and private-mode metadata stripping; updates docs and examples accordingly.

Reviewed changes

Copilot reviewed 45 out of 47 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
README.md Updates install sections and adds container quick-start + env var list updates.
pkg/storage/storage_test.go Uses filepath.Join for cross-platform path correctness in tests.
pkg/server/services/receive_service.go Adds per-file transfer state + atomic claim/complete/fail APIs; adjusts session close semantics.
pkg/server/services/receive_service_test.go Adds tests for claim/complete behavior including concurrency.
pkg/server/server.go Supports loading custom TLS cert/key and computing advertised fingerprint from leaf cert.
pkg/server/handlers/receive_upload.go Uses atomic file-claim flow, improves clipboard fallback/save behavior, path normalization, and error handling.
pkg/server/handlers/receive_handlers.go Adds clipboard-message short-circuit path, filename sanitization, negative-size guard, and misc sanitization.
pkg/server/handlers/receive_handlers_test.go Adds tests for path traversal, save failure, sanitization, and negative size handling.
pkg/server/handlers/prompt.go Sanitizes remote-controlled strings in prompts; adds clipboard prompt flow.
pkg/server/handlers/exec.go Adds placeholder replacement + configurable shell prefix for exec hooks.
pkg/server/handlers/discovery_handlers.go Sanitizes alias before logging register requests.
pkg/send/upload.go Refactors upload to support streaming sources and shared upload helper.
pkg/send/send.go Adds in-memory send option, smarter subnet scan, improved private-mode stripping flow, and clipboard short-circuit handling.
pkg/network/interfaces.go Adds interface-aware “usable subnet” IP generation with /22 cap.
pkg/metadata/strip.go Refactors stripping to support StripTo + atomic writes; expands PNG chunk stripping.
pkg/metadata/strip_test.go Adds tests for stripping behavior and edge cases.
pkg/help/help.go Updates main usage to include new commands.
pkg/help/commands.go Adds help entries for stop/config/version and daemon flag in serve help.
pkg/discovery/multicast.go Ensures re-announce updates device fields beyond LastSeen.
pkg/config/dto.go Prefers custom TLS fingerprint when present.
pkg/config/config.go Adds new config fields (shell/clipboard cmds/tls paths/notification cmd) and loads them from viper.
pkg/clipboard/clipboard.go Improves error messages, clipboard read handling, and adds override support.
pkg/clipboard/clipboard_windows.go Prefers PowerShell Set-Clipboard for UTF-8/unicode correctness; clip.exe fallback.
pkg/cli/sanitize.go Adds ANSI escape stripping helper for untrusted display strings.
pkg/cli/progress.go Avoids emitting VT100 cursor clears when stderr isn’t a terminal.
pkg/cli/output.go Sanitizes device alias in displayed tables/pickers.
pkg/cli/notify.go Adds optional custom notification command.
go.sum Updates sums for module changes (x/sys bump, x/term added).
go.mod Bumps golang.org/x/sys and adds golang.org/x/term.
docs/LIBRARY_GUIDE.md Updates examples/signatures and removes emoji headings for consistency.
docs/CONFIGURATION.md Updates precedence, flags/env vars, and defaults to match new behavior.
docs/CODE_WALKTHROUGH.md Updates architecture/package descriptions to match current structure.
docs/CLI_REFERENCE.md Adds global flags and new subcommands/flags, updates command semantics.
cmd/localgo/cmd/version.go Uses help system for version command help output.
cmd/localgo/cmd/stop.go Adds stop command to terminate daemon via PID file.
cmd/localgo/cmd/stop_windows.go Implements Windows daemon stop behavior via kill semantics.
cmd/localgo/cmd/stop_unix.go Implements Unix daemon stop behavior via SIGTERM + polling + SIGKILL.
cmd/localgo/cmd/serve.go Adds --daemon/-d support and daemon-child behavior tweaks.
cmd/localgo/cmd/send.go Adds in-memory clipboard/stdin send path and hostname resolution fallback for --ip.
cmd/localgo/cmd/scan.go Uses usable-subnet scanning instead of fixed /24 generation.
cmd/localgo/cmd/root.go Adds -v shorthand and wires clipboard/notification overrides from config.
cmd/localgo/cmd/pid.go Adds shared PID file path helper.
cmd/localgo/cmd/discover.go Uses usable-subnet scanning for discovery fallback.
cmd/localgo/cmd/daemon_windows.go Explicitly rejects daemon mode on Windows.
cmd/localgo/cmd/daemon_unix.go Implements daemon forking and PID file management on Unix.
cmd/localgo/cmd/config.go Fixes config-dir creation and wires help for config command.
.gitignore Broadens log ignore and adjusts config/mise ignore entries.
Comments suppressed due to low confidence (1)

pkg/server/handlers/receive_handlers.go:151

  • Same issue as above: when falling back to saving clipboard text as a file, history logging and exec hooks use the raw remote alias (requestDto.Info.Alias), which can inject escape sequences into localgo history output. Store/pass a sanitized alias instead.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/server/handlers/exec.go Outdated
Comment thread pkg/send/send.go
Comment thread pkg/clipboard/clipboard.go
Comment thread pkg/server/handlers/receive_handlers.go
Comment thread pkg/server/handlers/receive_handlers.go Outdated
@bethropolis
bethropolis merged commit 887f353 into main Jul 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants