159 lines
5.7 KiB
Go
159 lines
5.7 KiB
Go
package main
|
|
|
|
// console_test.go — every child process this daemon starts on Windows must be
|
|
// started with no console window.
|
|
//
|
|
// WHY A SOURCE-LEVEL TEST. The defect is invisible on the platform the tests run
|
|
// on: on Linux there is no console to allocate, so a behavioural test of
|
|
// hideConsole passes vacuously here and the real thing can only be seen by a
|
|
// person looking at a Windows desktop. What CAN be checked anywhere is the
|
|
// property that actually matters — that no exec site was left out. The last
|
|
// one was found by watching two blue windows flash every ten minutes
|
|
// (console_windows.go); this is what stops the next shell-out doing it again.
|
|
//
|
|
// It asks the toolchain which files are in a Windows build (go/build's own
|
|
// MatchFile, as buildtags_test.go does) rather than reading name suffixes, so a
|
|
// file behind any constraint form is judged correctly.
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// exemptFromHideConsole: files whose exec sites are deliberately different.
|
|
var exemptFromHideConsole = map[string]string{
|
|
// The self-update relaunch MUST keep DETACHED_PROCESS — that flag is the
|
|
// whole point, it is what lets the replacement outlive the process being
|
|
// replaced. It has no console by construction and starts nothing visible.
|
|
"relaunch_windows.go": "sets DETACHED_PROCESS deliberately",
|
|
// The approvals popup starts Edge itself: a GUI program with no console to
|
|
// hide, where hideConsole's HideWindow (SW_HIDE in the start-up info) would
|
|
// ask Edge to open its first window hidden — the popup, invisible. Its one
|
|
// exec site is the Edge launch; the browser fallback goes through
|
|
// openBrowser, which hides rundll32's console as every other site does.
|
|
"uipopup_windows.go": "starts a GUI window that must be seen",
|
|
}
|
|
|
|
// isExecCommand reports whether a call is exec.Command or exec.CommandContext.
|
|
func isExecCommand(call *ast.CallExpr) bool {
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
pkg, ok := sel.X.(*ast.Ident)
|
|
if !ok || pkg.Name != "exec" {
|
|
return false
|
|
}
|
|
return sel.Sel.Name == "Command" || sel.Sel.Name == "CommandContext"
|
|
}
|
|
|
|
// SCOPE, because a guard is only as wide as its walk:
|
|
// this reads THIS directory only. The bastion is a single-package module today
|
|
// — `go list ./...` is one package — so that is every exec site there is. The
|
|
// day a subpackage appears, its shell-outs are invisible here and this check
|
|
// goes on passing while the thing it protects stops being true. Widen the walk
|
|
// then; do not assume the walk widened itself.
|
|
func TestEveryWindowsExecSiteHidesItsConsole(t *testing.T) {
|
|
entries, err := os.ReadDir(".")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
win := build.Default
|
|
win.GOOS = "windows"
|
|
|
|
checked := 0
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
|
continue
|
|
}
|
|
if _, skip := exemptFromHideConsole[name]; skip {
|
|
continue
|
|
}
|
|
onWin, err := win.MatchFile(".", name)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
if !onWin {
|
|
continue // never compiled for Windows; there is no window to hide
|
|
}
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, name, nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
|
|
// One top-level declaration at a time, so a `var x = func(){…}` (as
|
|
// observe_ops.go uses) is one scope and the name match stays local.
|
|
for _, decl := range f.Decls {
|
|
var bound []struct {
|
|
name string
|
|
line int
|
|
}
|
|
hidden := map[string]bool{}
|
|
|
|
ast.Inspect(decl, func(n ast.Node) bool {
|
|
switch node := n.(type) {
|
|
case *ast.AssignStmt:
|
|
// x := exec.Command(…) — the shape that CAN be hidden.
|
|
if len(node.Rhs) == 1 && len(node.Lhs) >= 1 {
|
|
if call, ok := node.Rhs[0].(*ast.CallExpr); ok && isExecCommand(call) {
|
|
if id, ok := node.Lhs[0].(*ast.Ident); ok {
|
|
bound = append(bound, struct {
|
|
name string
|
|
line int
|
|
}{id.Name, fset.Position(node.Pos()).Line})
|
|
}
|
|
}
|
|
}
|
|
case *ast.CallExpr:
|
|
// hideConsole(x)
|
|
if id, ok := node.Fun.(*ast.Ident); ok && id.Name == "hideConsole" {
|
|
if len(node.Args) == 1 {
|
|
if arg, ok := node.Args[0].(*ast.Ident); ok {
|
|
hidden[arg.Name] = true
|
|
}
|
|
}
|
|
}
|
|
// exec.Command(…).Output() — nothing is bound, so nothing
|
|
// can be hidden. Must be split into a variable first.
|
|
if sel, ok := node.Fun.(*ast.SelectorExpr); ok {
|
|
if inner, ok := sel.X.(*ast.CallExpr); ok && isExecCommand(inner) {
|
|
t.Errorf("%s:%d — exec.Command(…).%s() is started inline, so no "+
|
|
"hideConsole can be applied and it opens a console window on "+
|
|
"Windows. Assign it first:\n"+
|
|
"\tcmd := exec.CommandContext(…)\n\thideConsole(cmd)\n\t… cmd.%s()",
|
|
name, fset.Position(node.Pos()).Line, sel.Sel.Name, sel.Sel.Name)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
for _, b := range bound {
|
|
checked++
|
|
if !hidden[b.name] {
|
|
t.Errorf("%s:%d — %s is an exec.Cmd that is never passed to hideConsole, "+
|
|
"so on Windows it opens a console window on the user's desktop every "+
|
|
"time it runs. Add `hideConsole(%s)` before it is started (and AFTER "+
|
|
"any assignment to %s.SysProcAttr, which would otherwise replace the "+
|
|
"flags). See console_windows.go.",
|
|
name, b.line, b.name, b.name, b.name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A guard that checked nothing would pass for ever after a refactor moved
|
|
// every exec site behind a helper this walk does not recognise.
|
|
if checked < 8 {
|
|
t.Fatalf("only %d exec site(s) examined — this check has stopped finding them, "+
|
|
"which makes it a green light that proves nothing", checked)
|
|
}
|
|
}
|