Skip to content

refactor: centralize safe command execution and native file ops#17

Open
mjc wants to merge 16 commits into
mainfrom
mjc/cmd-argv-cleanups
Open

refactor: centralize safe command execution and native file ops#17
mjc wants to merge 16 commits into
mainfrom
mjc/cmd-argv-cleanups

Conversation

@mjc

@mjc mjc commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

This refactor cleans up non-script.go command execution across the Go server by moving fixed service-control calls away from ad-hoc shell usage and replacing several shell-driven file/process operations with argv-form execution or native Go/Linux APIs.

The branch now focuses on three things:

  • standardizing fixed command execution through shared utils helpers,
  • preserving init-script compatibility by retrying script-like paths through sh on ENOEXEC,
  • replacing plain shell file/process operations like touch, rm, kill, sync, and hostname -F where native code is clearer and safer.

Intentional shell-facing paths like vm/script.go, the terminal shell, and passwd remain out of scope.

Bugs shaken out during the cleanup

While tightening these paths, this branch also fixed a few real behavior bugs that were worth keeping:

  • mdns PID parsing now rejects zero/negative values before syscall.Kill, instead of accepting Linux special PID forms.
  • hostname updates now fail the request if the live Sethostname call fails, instead of reporting success after only updating the files on disk.
  • virtual-device toggles now roll back the marker file on failed enable and best-effort restart S03usbdev on failed disable paths, so state is less likely to drift after partial failure.
  • USB gadget reset failures are now logged with the underlying error instead of disappearing behind a generic response.
  • tailscale login URL extraction now returns an actual validated URL token rather than stripping whitespace from any line containing https.
  • update / offline update / TLS flows now at least log restart failures instead of silently discarding them.

中文简要说明

这个 PR 主要是在 不碰 vm/script.go 的前提下,把 Go server 里这类固定命令执行路径整理干净:

  1. 把固定服务控制命令尽量统一到共享 helper。
  2. 把一些原本通过 shell 做的简单文件/进程操作改成原生 Go / Linux API。
  3. 保留 init 脚本兼容性:如果直接执行脚本遇到 ENOEXEC,会回退到 sh 执行,避免行为回退。

另外,这次整理过程中也顺手修掉了一些真实问题:

  • mdns PID 校验现在会拒绝 0 和负数;
  • hostname 只有在 live hostname 真的应用成功后才返回成功;
  • virtual-device 在部分失败时会做更合理的回滚 / 恢复;
  • USB gadget reset 失败现在会记录底层错误;
  • tailscale login URL 提取现在会返回真正校验过的 URL;
  • update / TLS 相关路径不再把 restart 失败完全吞掉。

重点还是两件事:

  • 减少这层代码里到处散落的命令执行写法;
  • 避免简单文件/进程操作继续依赖 shell。

vm/script.go、terminal shell、passwd 这些有意保留的路径仍然不在这个 PR 范围内。

Testing

Added and updated tests to show the refactor preserves behavior, including:

  • shared command helpers still pass argv correctly, preserve shell-script execution through ENOEXEC fallback, and stop command sequences on the first failure,
  • tailscale status/login parsing still handles noisy output and extracts the expected login URL,
  • USB gadget reset logic still clears with a newline write, rebinds the discovered controller, and fails cleanly when no controller is present,
  • mdns PID parsing still accepts valid PIDs and rejects empty, zero, negative, and malformed values.

Copilot AI review requested due to automatic review settings June 8, 2026 16:16
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors command execution across the NanoKVM server by introducing centralized utilities (command execution with retry logic, atomic file copy, service restart) and updating all service files to use these helpers instead of direct exec.Command calls, with selective syscall integration for DNS, hostname, and process signaling.

Changes

Command Execution Centralization

Layer / File(s) Summary
Utility Foundation: Command Execution & File Copy
server/utils/command.go, server/utils/command_test.go, server/utils/copy_file.go, server/utils/copy_file_test.go, server/utils/restart.go
New CommandSpec type and Run(), RunOutput(), RunContext(), RunOutputContext() wrappers execute commands with automatic sh retry on ENOEXEC. RunSequence() and RunSequenceWithDelay() run ordered command lists with optional inter-command delays. CopyFile() performs atomic copy via temp file with mode preservation. RestartNanoKVM() invokes the init script. Comprehensive tests cover argument forwarding, script execution without shebang, context cancellation, and sequencing behavior.
Simple Execution Replacements: Application, HID, Network, VM
server/service/application/update.go, server/service/application/update_offline.go, server/service/hid/status.go, server/service/network/wifi.go, server/service/network/wol.go, server/service/vm/ssh.go, server/service/vm/system.go, server/service/vm/tls.go
Eighteen direct replacements of exec.Command() invocations with utils.Run() or utils.RunOutput(). All files remove os/exec imports and add utils imports. Changes include: update restarts, reboot, USB PHY reset, Wi-Fi stop, WoL ether-wake, SSH enable/disable, system reboot, and TLS restart.
Syscall Integration & mDNS Refactor
server/service/network/dns.go, server/service/vm/hostname.go, server/service/vm/mdns.go
DNS SetDNS calls syscall.Sync() instead of external sync command. SetHostname applies hostname via syscall.Sethostname() instead of hostname command. mDNS adds parseAvahiPID helper to validate PID numerically, uses syscall.Kill() with SIGKILL instead of shell kill command, and uses utils.CopyFile to atomically install Avahi script.
PicoClaw Runtime Detection & Startup
server/service/picoclaw/runtime_start_stop.go, server/service/picoclaw/runtime_start_stop_test.go
PicoClaw startup/stop/onboard invoke runPicoclawScript helper using utils.RunOutputContext instead of inline sh -c construction. Runtime liveness detection replaces pidof with /proc scanning: new isProcessRunning helper scans /proc/<pid>/comm entries to match binary names. Tests validate process matching and rejection of non-matching entries.
Storage USB Gadget Reset & Virtual Device Mounting
server/service/storage/image.go, server/service/storage/image_test.go, server/service/vm/virtual-device.go
MountImage delegates USB gadget UDC reset to resetUSBGadgetUDC() helper that clears the UDC sysfs node, sleeps, discovers the first controller under /sys/class/udc, then rebinds. UpdateVirtualDevice replaces shell command iteration with typed action func() selected per device type and existence, calling mount/unmount helpers that use utils.Run for script execution, os.RemoveAll for gadget cleanup, and os.WriteFile for device files. Tests verify UDC rebinding and error handling when no controllers exist.
VM Swap Sequencing & Command Runners
server/service/vm/swap.go, server/service/vm/command_runner.go, server/service/vm/command_helpers_test.go
enableSwap builds a commandSpec list via buildSwapCommands (fallocate, chmod, mkswap, swapon) and executes via runCommandSpecs with 300ms inter-command delay, replacing inline loop. disableSwap uses runCommandSpecs for swapoff. New command_runner.go wraps utils.RunSequenceWithDelay. Test validates parseAvahiPID numeric validation, whitespace trimming, and empty-input rejection.
Tailscale CLI Refactoring: Helpers & JSON Parsing
server/service/extensions/tailscale/cli.go, server/service/extensions/tailscale/cli_test.go
Tailscale CLI consolidates execution via new helpers: runInitScriptAction for script actions, runTailscale/runTailscaleOutput for Tailscale commands, parseStatusOutput to strip leading preamble before JSON parsing, extractLoginURL to trim whitespace and validate login URL. All operations replace shell command string construction and direct exec invocation. Tests verify preamble handling and URL extraction edge cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hops through exec paths old and worn,
New utils born to standardize command,
Syscalls bloom where shells were worn,
Process scanning by /proc's careful hand,
Utilities refactor, clean and true—
The server's heartbeat beats anew! 🐾

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title directly summarizes the main change: refactoring command execution and file operations to use centralized, safer helpers instead of shell-form invocations.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mjc/cmd-argv-cleanups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

This PR refactors multiple service paths to stop using shell-form sh -c execution and instead run commands via argv-style exec.Command(...), reducing shell parsing/injection risk and improving argument handling. It also replaces a USB gadget reset sequence that previously relied on shell redirection/pipelines with direct sysfs file operations.

Changes:

  • Replace exec.Command("sh","-c", "...") with argv-form exec.Command(bin, args...) across VM, application update, networking, HID, and Tailscale service paths.
  • Refactor chained shell && sequences into explicit sequential command execution with error checks (e.g., cp then start).
  • Implement USB gadget UDC reset via os.WriteFile + os.ReadDir rather than shell pipelines/redirection.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
server/service/vm/virtual-device.go Converts virtual device mount/unmount command lists from shell strings to structured argv execution.
server/service/vm/tls.go Uses argv-form init script restart instead of sh -c.
server/service/vm/swap.go Uses argv-form swap setup/teardown commands and improves logging detail.
server/service/vm/ssh.go Runs SSH init script with explicit args instead of formatting a shell command.
server/service/vm/mdns.go Replaces shell chaining and kill command string with explicit argv commands and PID validation.
server/service/storage/image.go Replaces shell-based USB reset with direct sysfs writes via resetUSBGadgetUDC().
server/service/picoclaw/runtime_start_stop.go Runs Picoclaw script actions via a helper wrapping exec.CommandContext argv form.
server/service/network/wifi.go Uses argv-form to stop WiFi script instead of sh -c.
server/service/hid/status.go Uses argv-form to call USB device script action restart_phy.
server/service/extensions/tailscale/cli.go Replaces shell commands with argv-form for script lifecycle and tailscale CLI operations.
server/service/application/update.go Uses argv-form restart of S95nanokvm after update.
server/service/application/update_offline.go Uses argv-form restart of S95nanokvm after offline update.

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

Comment thread server/service/storage/image.go
@mjc mjc changed the title Refactor non-script command runners to argv form refactor: replace shell-form command runners with argv execution helpers Jun 8, 2026
@mjc
mjc requested a review from Copilot June 8, 2026 16:29
@mjc
mjc force-pushed the mjc/cmd-argv-cleanups branch from bdde989 to dfc4618 Compare June 8, 2026 16:30

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

mjc added 4 commits June 8, 2026 10:34
Replace shell-form command execution with argv-based exec calls across
application, vm, tailscale, network, storage, hid, and picoclaw services,
while leaving vm/script.go untouched for the dedicated script-runner branch.
Add source-level guard tests for refactored command execution paths and
new helper-level tests in vm for PID parsing, swap command building,
and virtual-device argv command specs.
Update the mount-image error message to reflect the refactored
resetUSBGadgetUDC path instead of the old generic command wording.
Refactor repeated argv command patterns into shared helpers across tailscale,
vm command sequences, and NanoKVM restart paths. Add targeted tests for new
helpers and update regression checks accordingly.
@mjc
mjc force-pushed the mjc/cmd-argv-cleanups branch from dfc4618 to e506eef Compare June 8, 2026 16:34
@mjc
mjc requested a review from Copilot June 8, 2026 16:34
Further collapse tailscale command wrappers and simplify PR guard tests to
focus on key signals rather than exhaustive snippet tables.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread server/tests/command_exec_refactor_test.go Outdated
mjc and others added 8 commits June 8, 2026 10:45
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mjc
mjc requested a review from Copilot June 8, 2026 17:18
@mjc mjc changed the title refactor: replace shell-form command runners with argv execution helpers refactor: centralize safe command execution and native file ops Jun 8, 2026

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

Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.

Comment thread server/service/vm/mdns.go
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/service/application/update.go`:
- Line 43: The call to utils.RestartNanoKVM() is currently discarding errors;
change the call from ignoring the result to capturing the error (err :=
utils.RestartNanoKVM()) and handle failures by logging the error and returning a
failure from the surrounding update function (or wrapping and returning the
error) so a successful response isn't returned when the restart fails; ensure
the surrounding function that calls utils.RestartNanoKVM (the update handler in
update.go) does not emit success before the restart completes and propagate the
error up to the caller.

In `@server/service/extensions/tailscale/cli.go`:
- Around line 99-100: The code returns whatever extractLoginURL(line) finds and
strips whitespace from the entire line which can produce malformed output;
update extractLoginURL (and its call sites) to locate the URL token (e.g., split
the line into tokens or use a regex), trim surrounding punctuation/whitespace
(not remove internal whitespace), and validate it using net/url.Parse ensuring a
valid scheme (http/https) and non-empty host before returning; if validation
fails, continue scanning rather than returning the raw line.

In `@server/service/picoclaw/runtime_start_stop.go`:
- Around line 237-242: The per-PID os.ReadFile(commPath) call currently returns
the whole function on any read error; change this so only os.ReadDir(procRoot)
failures remain fatal and per-PID read errors are treated as skip-and-continue
(optionally log/debug the error) instead of returning false, err—i.e., inside
the loop around commPath/ReadFile, on error if os.IsNotExist(err) or any other
read error, continue to the next PID rather than returning; update the ReadFile
error handling near the commPath variable in runtime_start_stop.go accordingly.

In `@server/service/storage/image_test.go`:
- Around line 9-58: The tests for resetUSBGadgetUDCPaths don't verify that the
"clear" write to the UDC file is a non-empty newline (which is the required
sysfs behavior); update resetUSBGadgetUDCPaths to accept an injectable write
helper (e.g., a writer function or interface param used for the two writes:
clear and rebind) and then change the tests to use a fake writer that records
each write call and asserts the first write is exactly "\n" (not zero-length)
and the second write contains the controller name "musb-hdrc.0"; keep function
name resetUSBGadgetUDCPaths and tests
TestResetUSBGadgetUDCPathsRebindsFirstController/TestResetUSBGadgetUDCPathsFailsWithoutController
so the diff is limited to adding the writer injection and asserting the newline
invariant in the test harness.

In `@server/service/storage/image.go`:
- Around line 130-132: The error returned from resetUSBGadgetUDC is never
logged; update the branch that handles its error so the underlying error is
recorded and included in the API response: when resetUSBGadgetUDC() returns err,
log the error using the project's logger (e.g., log.Errorf or the existing
server logger) including context like "reset usb gadget failed" and/or include
err.Error() in the rsp.ErrRsp call (replace the static message "reset usb gadget
failed" with a message that appends err.Error() or pass the error string as the
response detail) so both logs and the API response reflect the actual failure
from resetUSBGadgetUDC.

In `@server/service/vm/hostname.go`:
- Around line 65-68: The handler currently calls rsp.OkRsp(c) before applying
the live hostname and ignores errors from syscall.Sethostname(data); move the
rsp.OkRsp(c) call so it only runs after syscall.Sethostname succeeds, check the
return value of syscall.Sethostname(data) and if it returns an error log and
return an appropriate error response (do not swallow the error), and ensure the
function returns immediately on failure; update code around the
syscall.Sethostname and rsp.OkRsp calls (in the hostname handler function) to
reflect this flow.

In `@server/service/vm/virtual-device.go`:
- Around line 108-113: mountVirtualDeviceNetwork currently leaves the marker
created by ensureVirtualDeviceFile(virtualNetwork) if restartUSBDeviceScript()
fails; modify mountVirtualDeviceNetwork so that after ensureVirtualDeviceFile
returns success you attempt restartUSBDeviceScript() and on any error you
remove/rollback the marker (i.e. delete the file created for virtualNetwork)
before returning the error; implement the same rollback pattern for the
corresponding disk activation function (the similar block around
ensureVirtualDeviceFile and restartUSBDeviceScript for the disk path) so that
GetVirtualDevice() remains consistent when restart fails.
- Around line 115-125: unmountVirtualDeviceNetwork currently returns immediately
on intermediate errors after calling utils.Run(usbDevScript, "stop"), leaving
S03usbdev down; change it to record that stop succeeded and install a deferred
best-effort restart (call utils.Run(usbDevScript, "start")) that always runs
unless the function performs the final explicit restart successfully, and
suppress that defer when the final utils.Run succeeds; apply the same pattern to
the analogous function handling the other path (the function around lines
135-145) and reference usbDevScript, utils.Run, networkConfigPath and
virtualNetwork when making the change so the defer only runs if stop succeeded
and is cancelled when the final restart succeeds.

In `@server/utils/command.go`:
- Around line 64-66: The RunSequenceWithDelay function is sleeping after the
final successful command, adding unnecessary tail latency; change its loop so
time.Sleep(delay) is only invoked between commands (i.e., when there is a next
command) and not after the last one. Concretely, in RunSequenceWithDelay check
that the current index is not the last (e.g., i < len(cmds)-1) and that the
command succeeded before calling time.Sleep(delay), or move the sleep to the
start of the next-iteration path; keep existing handling of errors and the delay
variable unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6bd79e49-3fc0-4ce0-8d34-8b1465acd7ab

📥 Commits

Reviewing files that changed from the base of the PR and between 3de4a18 and 2d7e743.

📒 Files selected for processing (26)
  • server/service/application/update.go
  • server/service/application/update_offline.go
  • server/service/extensions/tailscale/cli.go
  • server/service/extensions/tailscale/cli_test.go
  • server/service/hid/status.go
  • server/service/network/dns.go
  • server/service/network/wifi.go
  • server/service/network/wol.go
  • server/service/picoclaw/runtime_start_stop.go
  • server/service/picoclaw/runtime_start_stop_test.go
  • server/service/storage/image.go
  • server/service/storage/image_test.go
  • server/service/vm/command_helpers_test.go
  • server/service/vm/command_runner.go
  • server/service/vm/hostname.go
  • server/service/vm/mdns.go
  • server/service/vm/ssh.go
  • server/service/vm/swap.go
  • server/service/vm/system.go
  • server/service/vm/tls.go
  • server/service/vm/virtual-device.go
  • server/utils/command.go
  • server/utils/command_test.go
  • server/utils/copy_file.go
  • server/utils/copy_file_test.go
  • server/utils/restart.go

Comment thread server/service/application/update.go Outdated
Comment thread server/service/extensions/tailscale/cli.go
Comment thread server/service/picoclaw/runtime_start_stop.go Outdated
Comment thread server/service/storage/image_test.go
Comment thread server/service/storage/image.go
Comment thread server/service/vm/hostname.go Outdated
Comment thread server/service/vm/virtual-device.go Outdated
Comment thread server/service/vm/virtual-device.go Outdated
Comment thread server/utils/command.go Outdated
mjc and others added 2 commits June 8, 2026 11:28
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comment thread server/utils/command.go
Comment on lines +93 to +103
if _, err := io.Copy(tmpFile, srcFile); err != nil {
_ = tmpFile.Close()
return err
}
if err := tmpFile.Chmod(info.Mode()); err != nil {
_ = tmpFile.Close()
return err
}
if err := tmpFile.Close(); err != nil {
return err
}
Comment on lines +64 to +72
func writeScript(t *testing.T, content string) string {
t.Helper()

scriptPath := filepath.Join(t.TempDir(), "script")
if err := os.WriteFile(scriptPath, []byte(content), 0o755); err != nil {
t.Fatalf("write script: %v", err)
}
return scriptPath
}
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