1// +build windows
2
3package runtime_test
4
5import (
6	"bufio"
7	"bytes"
8	"fmt"
9	"internal/testenv"
10	"os"
11	"os/exec"
12	"path/filepath"
13	"runtime"
14	"strconv"
15	"strings"
16	"syscall"
17	"testing"
18)
19
20func TestVectoredHandlerDontCrashOnLibrary(t *testing.T) {
21	if *flagQuick {
22		t.Skip("-quick")
23	}
24	if runtime.GOARCH != "amd64" {
25		t.Skip("this test can only run on windows/amd64")
26	}
27	testenv.MustHaveGoBuild(t)
28	testenv.MustHaveExecPath(t, "gcc")
29	testprog.Lock()
30	defer testprog.Unlock()
31	dir, err := os.MkdirTemp("", "go-build")
32	if err != nil {
33		t.Fatalf("failed to create temp directory: %v", err)
34	}
35	defer os.RemoveAll(dir)
36
37	// build go dll
38	dll := filepath.Join(dir, "testwinlib.dll")
39	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", dll, "--buildmode", "c-shared", "testdata/testwinlib/main.go")
40	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
41	if err != nil {
42		t.Fatalf("failed to build go library: %s\n%s", err, out)
43	}
44
45	// build c program
46	exe := filepath.Join(dir, "test.exe")
47	cmd = exec.Command("gcc", "-L"+dir, "-I"+dir, "-ltestwinlib", "-o", exe, "testdata/testwinlib/main.c")
48	out, err = testenv.CleanCmdEnv(cmd).CombinedOutput()
49	if err != nil {
50		t.Fatalf("failed to build c exe: %s\n%s", err, out)
51	}
52
53	// run test program
54	cmd = exec.Command(exe)
55	out, err = testenv.CleanCmdEnv(cmd).CombinedOutput()
56	if err != nil {
57		t.Fatalf("failure while running executable: %s\n%s", err, out)
58	}
59	expectedOutput := "exceptionCount: 1\ncontinueCount: 1\n"
60	// cleaning output
61	cleanedOut := strings.ReplaceAll(string(out), "\r\n", "\n")
62	if cleanedOut != expectedOutput {
63		t.Errorf("expected output %q, got %q", expectedOutput, cleanedOut)
64	}
65}
66
67func sendCtrlBreak(pid int) error {
68	kernel32, err := syscall.LoadDLL("kernel32.dll")
69	if err != nil {
70		return fmt.Errorf("LoadDLL: %v\n", err)
71	}
72	generateEvent, err := kernel32.FindProc("GenerateConsoleCtrlEvent")
73	if err != nil {
74		return fmt.Errorf("FindProc: %v\n", err)
75	}
76	result, _, err := generateEvent.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid))
77	if result == 0 {
78		return fmt.Errorf("GenerateConsoleCtrlEvent: %v\n", err)
79	}
80	return nil
81}
82
83// TestCtrlHandler tests that Go can gracefully handle closing the console window.
84// See https://golang.org/issues/41884.
85func TestCtrlHandler(t *testing.T) {
86	testenv.MustHaveGoBuild(t)
87	t.Parallel()
88
89	// build go program
90	exe := filepath.Join(t.TempDir(), "test.exe")
91	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", exe, "testdata/testwinsignal/main.go")
92	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
93	if err != nil {
94		t.Fatalf("failed to build go exe: %v\n%s", err, out)
95	}
96
97	// run test program
98	cmd = exec.Command(exe)
99	var stderr bytes.Buffer
100	cmd.Stderr = &stderr
101	outPipe, err := cmd.StdoutPipe()
102	if err != nil {
103		t.Fatalf("Failed to create stdout pipe: %v", err)
104	}
105	outReader := bufio.NewReader(outPipe)
106
107	// in a new command window
108	const _CREATE_NEW_CONSOLE = 0x00000010
109	cmd.SysProcAttr = &syscall.SysProcAttr{
110		CreationFlags: _CREATE_NEW_CONSOLE,
111		HideWindow:    true,
112	}
113	if err := cmd.Start(); err != nil {
114		t.Fatalf("Start failed: %v", err)
115	}
116	defer func() {
117		cmd.Process.Kill()
118		cmd.Wait()
119	}()
120
121	// wait for child to be ready to receive signals
122	if line, err := outReader.ReadString('\n'); err != nil {
123		t.Fatalf("could not read stdout: %v", err)
124	} else if strings.TrimSpace(line) != "ready" {
125		t.Fatalf("unexpected message: %s", line)
126	}
127
128	// gracefully kill pid, this closes the command window
129	if err := exec.Command("taskkill.exe", "/pid", strconv.Itoa(cmd.Process.Pid)).Run(); err != nil {
130		t.Fatalf("failed to kill: %v", err)
131	}
132
133	// check child received, handled SIGTERM
134	if line, err := outReader.ReadString('\n'); err != nil {
135		t.Fatalf("could not read stdout: %v", err)
136	} else if expected, got := syscall.SIGTERM.String(), strings.TrimSpace(line); expected != got {
137		t.Fatalf("Expected '%s' got: %s", expected, got)
138	}
139
140	// check child exited gracefully, did not timeout
141	if err := cmd.Wait(); err != nil {
142		t.Fatalf("Program exited with error: %v\n%s", err, &stderr)
143	}
144}
145
146// TestLibraryCtrlHandler tests that Go DLL allows calling program to handle console control events.
147// See https://golang.org/issues/35965.
148func TestLibraryCtrlHandler(t *testing.T) {
149	if *flagQuick {
150		t.Skip("-quick")
151	}
152	if runtime.GOARCH != "amd64" {
153		t.Skip("this test can only run on windows/amd64")
154	}
155	testenv.MustHaveGoBuild(t)
156	testenv.MustHaveExecPath(t, "gcc")
157	testprog.Lock()
158	defer testprog.Unlock()
159	dir, err := os.MkdirTemp("", "go-build")
160	if err != nil {
161		t.Fatalf("failed to create temp directory: %v", err)
162	}
163	defer os.RemoveAll(dir)
164
165	// build go dll
166	dll := filepath.Join(dir, "dummy.dll")
167	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", dll, "--buildmode", "c-shared", "testdata/testwinlibsignal/dummy.go")
168	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
169	if err != nil {
170		t.Fatalf("failed to build go library: %s\n%s", err, out)
171	}
172
173	// build c program
174	exe := filepath.Join(dir, "test.exe")
175	cmd = exec.Command("gcc", "-o", exe, "testdata/testwinlibsignal/main.c")
176	out, err = testenv.CleanCmdEnv(cmd).CombinedOutput()
177	if err != nil {
178		t.Fatalf("failed to build c exe: %s\n%s", err, out)
179	}
180
181	// run test program
182	cmd = exec.Command(exe)
183	var stderr bytes.Buffer
184	cmd.Stderr = &stderr
185	outPipe, err := cmd.StdoutPipe()
186	if err != nil {
187		t.Fatalf("Failed to create stdout pipe: %v", err)
188	}
189	outReader := bufio.NewReader(outPipe)
190
191	cmd.SysProcAttr = &syscall.SysProcAttr{
192		CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
193	}
194	if err := cmd.Start(); err != nil {
195		t.Fatalf("Start failed: %v", err)
196	}
197
198	errCh := make(chan error, 1)
199	go func() {
200		if line, err := outReader.ReadString('\n'); err != nil {
201			errCh <- fmt.Errorf("could not read stdout: %v", err)
202		} else if strings.TrimSpace(line) != "ready" {
203			errCh <- fmt.Errorf("unexpected message: %v", line)
204		} else {
205			errCh <- sendCtrlBreak(cmd.Process.Pid)
206		}
207	}()
208
209	if err := <-errCh; err != nil {
210		t.Fatal(err)
211	}
212	if err := cmd.Wait(); err != nil {
213		t.Fatalf("Program exited with error: %v\n%s", err, &stderr)
214	}
215}
216