From f4fb16c0ed1f7d874cf9b5ce13602278d0fc8ec0 Mon Sep 17 00:00:00 2001 From: mixeme Date: Thu, 18 Jun 2026 21:28:25 +0300 Subject: [PATCH] T1.2: Create src/platform/winproc; move configureHiddenWindow Extract the hidden-window logic out of src/core runner and autostart files into a new platform/winproc package with per-OS build-tag files. All call sites updated to use winproc.ConfigureHiddenWindow. Builds clean on both Windows and Linux; all tests pass. Co-Authored-By: Claude Sonnet 4.6 --- docs/REFACTORING.md | 2 +- src/core/autostart_windows.go | 9 +++++---- src/core/runner.go | 5 +++-- src/core/runner_other.go | 5 ----- src/core/runner_test.go | 5 +++-- src/core/runner_windows.go | 10 ---------- src/platform/winproc/winproc_other.go | 10 ++++++++++ src/platform/winproc/winproc_windows.go | 18 ++++++++++++++++++ 8 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 src/platform/winproc/winproc_other.go create mode 100644 src/platform/winproc/winproc_windows.go diff --git a/docs/REFACTORING.md b/docs/REFACTORING.md index 7ca1a98..415e3a1 100644 --- a/docs/REFACTORING.md +++ b/docs/REFACTORING.md @@ -245,7 +245,7 @@ Track progress here. Mark tasks complete as they land and pass review. ### Phase 1 — Split flat `core` package - [x] T1.1 — Create `src/domain`; move Job/RunRecord/Config/etc -- [ ] T1.2 — Create `src/platform/winproc`; move `configureHiddenWindow` +- [x] T1.2 — Create `src/platform/winproc`; move `configureHiddenWindow` - [ ] T1.3 — Create `src/runner`; move runner logic - [ ] T1.4 — Create `src/scheduler`; move scheduler - [ ] T1.5 — Create `src/storage`; move store/paths diff --git a/src/core/autostart_windows.go b/src/core/autostart_windows.go index 274b22f..157a207 100644 --- a/src/core/autostart_windows.go +++ b/src/core/autostart_windows.go @@ -8,6 +8,7 @@ import ( "strings" "gitea.mixdep.ru/mix/gosentry/src/domain" + "gitea.mixdep.ru/mix/gosentry/src/platform/winproc" ) const autostartName = "GoSentry" @@ -107,7 +108,7 @@ func createStartupShortcut(shortcutPath string, executablePath string, iconPath "GOSENTRY_WORKING_DIRECTORY="+workingDirectory, "GOSENTRY_ICON_PATH="+iconPath, ) - configureHiddenWindow(command) + winproc.ConfigureHiddenWindow(command) if output, err := command.CombinedOutput(); err != nil { return fmt.Errorf("create startup shortcut: %w: %s", err, strings.TrimSpace(string(output))) } @@ -125,7 +126,7 @@ func readShortcut(shortcutPath string) (string, string, error) { script := `[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false); $shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); [Console]::Out.Write($shortcut.TargetPath + [Environment]::NewLine + $shortcut.Arguments)` command := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script) command.Env = append(os.Environ(), "GOSENTRY_SHORTCUT_PATH="+shortcutPath) - configureHiddenWindow(command) + winproc.ConfigureHiddenWindow(command) output, err := command.CombinedOutput() if err != nil { return "", "", fmt.Errorf("read startup shortcut: %w: %s", err, strings.TrimSpace(string(output))) @@ -155,7 +156,7 @@ func removeIfExists(path string) error { func cleanupLegacyRegistryAutostart() error { for _, name := range []string{legacyAutostartName, autostartName} { command := exec.Command("reg.exe", "delete", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", name, "/f") - configureHiddenWindow(command) + winproc.ConfigureHiddenWindow(command) _ = command.Run() } return nil @@ -164,7 +165,7 @@ func cleanupLegacyRegistryAutostart() error { func legacyRegistryAutostartExists() bool { for _, name := range []string{legacyAutostartName, autostartName} { command := exec.Command("reg.exe", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", name) - configureHiddenWindow(command) + winproc.ConfigureHiddenWindow(command) if command.Run() == nil { return true } diff --git a/src/core/runner.go b/src/core/runner.go index 6e88f33..6f7fe86 100644 --- a/src/core/runner.go +++ b/src/core/runner.go @@ -15,6 +15,7 @@ import ( "unicode" "gitea.mixdep.ru/mix/gosentry/src/domain" + "gitea.mixdep.ru/mix/gosentry/src/platform/winproc" ) const commandTimeout = 30 * time.Second @@ -42,7 +43,7 @@ func RunJob(ctx context.Context, job *domain.Job, trigger string, logsDir string command := invocation.command command.WaitDelay = commandWaitDelay if invocation.hideWindow { - configureHiddenWindow(command) + winproc.ConfigureHiddenWindow(command) } command.Stdout = &stdout command.Stderr = &stderr @@ -176,7 +177,7 @@ func sanitizeFileName(name string) string { func startJobOnly(invocation commandInvocation, job domain.Job, started time.Time) (string, string, string) { command := invocation.command if invocation.hideWindow { - configureHiddenWindow(command) + winproc.ConfigureHiddenWindow(command) } err := command.Start() duration := time.Since(started).Round(time.Millisecond) diff --git a/src/core/runner_other.go b/src/core/runner_other.go index 5c94ead..b2c12e6 100644 --- a/src/core/runner_other.go +++ b/src/core/runner_other.go @@ -13,8 +13,3 @@ func shellCommand(ctx context.Context, command string) *exec.Cmd { return exec.CommandContext(ctx, "sh", "-c", command) } -func configureHiddenWindow(command *exec.Cmd) { - // Non-Windows platforms do not create a new console window for sh -c from a - // desktop process in the same way Windows does, so no extra process attribute - // is required here. -} diff --git a/src/core/runner_test.go b/src/core/runner_test.go index 5d22bdb..d61fa97 100644 --- a/src/core/runner_test.go +++ b/src/core/runner_test.go @@ -10,6 +10,7 @@ import ( "time" "gitea.mixdep.ru/mix/gosentry/src/domain" + "gitea.mixdep.ru/mix/gosentry/src/platform/winproc" ) func TestRunJobLogFileAllHeaders(t *testing.T) { @@ -377,7 +378,7 @@ func TestShellCommandHidesWindow(t *testing.T) { if !invocation.hideWindow { t.Fatal("shell command should request hidden startup window") } - configureHiddenWindow(invocation.command) + winproc.ConfigureHiddenWindow(invocation.command) if invocation.command.SysProcAttr == nil || !invocation.command.SysProcAttr.HideWindow { t.Fatal("expected shell command to be hidden") } @@ -389,7 +390,7 @@ func TestShellCommandUsesWindowsSafeQuoting(t *testing.T) { } command := shellCommand(context.Background(), `"C:\Program Files\FreeFileSync\FreeFileSync.exe" "D:\Local\Programs\FreeFileSync\Jobs\Auto.ffs_batch"`) - configureHiddenWindow(command) + winproc.ConfigureHiddenWindow(command) want := `cmd.exe /S /C ""C:\Program Files\FreeFileSync\FreeFileSync.exe" "D:\Local\Programs\FreeFileSync\Jobs\Auto.ffs_batch""` if command.SysProcAttr == nil { diff --git a/src/core/runner_windows.go b/src/core/runner_windows.go index fa37fff..f8764cf 100644 --- a/src/core/runner_windows.go +++ b/src/core/runner_windows.go @@ -57,13 +57,3 @@ func startsWithWindowsRootedPath(command string) bool { (command[2] == '\\' || command[2] == '/') } -func configureHiddenWindow(command *exec.Cmd) { - // GoSentry is a GUI scheduler, so child commands should not flash a console - // window on Windows. CREATE_NO_WINDOW keeps cmd.exe and simple console tools - // quiet while stdout/stderr are still captured through pipes. - if command.SysProcAttr == nil { - command.SysProcAttr = &syscall.SysProcAttr{} - } - command.SysProcAttr.CreationFlags |= 0x08000000 - command.SysProcAttr.HideWindow = true -} diff --git a/src/platform/winproc/winproc_other.go b/src/platform/winproc/winproc_other.go new file mode 100644 index 0000000..97d5b96 --- /dev/null +++ b/src/platform/winproc/winproc_other.go @@ -0,0 +1,10 @@ +//go:build !windows + +package winproc + +import "os/exec" + +// ConfigureHiddenWindow is a no-op on non-Windows platforms: launching sh -c +// from a desktop process does not create a new console window in the same way +// Windows does. +func ConfigureHiddenWindow(command *exec.Cmd) {} diff --git a/src/platform/winproc/winproc_windows.go b/src/platform/winproc/winproc_windows.go new file mode 100644 index 0000000..ddae028 --- /dev/null +++ b/src/platform/winproc/winproc_windows.go @@ -0,0 +1,18 @@ +package winproc + +import ( + "os/exec" + "syscall" +) + +// ConfigureHiddenWindow suppresses the console window that Windows would +// otherwise flash when running a child process from a GUI application. +// CREATE_NO_WINDOW keeps cmd.exe and simple console tools quiet while +// stdout/stderr are still captured through pipes. +func ConfigureHiddenWindow(command *exec.Cmd) { + if command.SysProcAttr == nil { + command.SysProcAttr = &syscall.SysProcAttr{} + } + command.SysProcAttr.CreationFlags |= 0x08000000 + command.SysProcAttr.HideWindow = true +}