From 927af8164e2ec8f315911459429a9e46c3dd10c5 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 9 Oct 2020 17:33:51 +0100 Subject: [PATCH 01/11] Console CtrlHandler routine should block Fixes #41884. --- src/runtime/os_windows.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go index bddc25729aa773..97da629ddf5841 100644 --- a/src/runtime/os_windows.go +++ b/src/runtime/os_windows.go @@ -45,6 +45,7 @@ const ( //go:cgo_import_dynamic runtime._SetThreadPriority SetThreadPriority%2 "kernel32.dll" //go:cgo_import_dynamic runtime._SetUnhandledExceptionFilter SetUnhandledExceptionFilter%1 "kernel32.dll" //go:cgo_import_dynamic runtime._SetWaitableTimer SetWaitableTimer%6 "kernel32.dll" +//go:cgo_import_dynamic runtime._Sleep Sleep%1 "kernel32.dll" //go:cgo_import_dynamic runtime._SuspendThread SuspendThread%1 "kernel32.dll" //go:cgo_import_dynamic runtime._SwitchToThread SwitchToThread%0 "kernel32.dll" //go:cgo_import_dynamic runtime._TlsAlloc TlsAlloc%0 "kernel32.dll" @@ -95,6 +96,7 @@ var ( _SetThreadPriority, _SetUnhandledExceptionFilter, _SetWaitableTimer, + _Sleep, _SuspendThread, _SwitchToThread, _TlsAlloc, @@ -1029,6 +1031,7 @@ func ctrlhandler1(_type uint32) uint32 { } if sigsend(s) { + stdcall1(_Sleep, uintptr(_INFINITE)) return 1 } exit(2) // SIGINT, SIGTERM, etc From b6fa87797b9c8bf323c1f11feed40c2b7bd42994 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Sat, 10 Oct 2020 01:49:54 +0100 Subject: [PATCH 02/11] Add test case. --- src/runtime/signal_windows_test.go | 66 ++++++++++++++++++++++ src/runtime/testdata/testwinsignal/main.go | 21 +++++++ 2 files changed, 87 insertions(+) create mode 100644 src/runtime/testdata/testwinsignal/main.go diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index f99857193c1ad0..07293502ca3822 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -8,6 +8,7 @@ import ( "fmt" "internal/testenv" "io/ioutil" + "net" "os" "os/exec" "path/filepath" @@ -15,6 +16,7 @@ import ( "strings" "syscall" "testing" + "time" ) func TestVectoredHandlerDontCrashOnLibrary(t *testing.T) { @@ -80,6 +82,70 @@ func sendCtrlBreak(pid int) error { return nil } +func TestCtrlHandler(t *testing.T) { + if *flagQuick { + t.Skip("-quick") + } + testenv.MustHaveGoBuild(t) + testenv.MustHaveExecPath(t, "gcc") + testprog.Lock() + defer testprog.Unlock() + dir, err := ioutil.TempDir("", "go-build") + if err != nil { + t.Fatalf("failed to create temp directory: %v", err) + } + defer os.RemoveAll(dir) + + // build go program + exe := filepath.Join(dir, "test.exe") + cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", exe, "testdata/testwinsignal/main.go") + out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() + if err != nil { + t.Fatalf("failed to build go exe: %v\n%s", err, out) + } + + // udp socket for synchronization + conn, err := net.ListenPacket("udp", "[::1]:0") + if err != nil { + t.Fatalf("ListenPacket failed: %v", err) + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(5 * time.Second)) + + // run test program + cmd = exec.Command("cmd.exe", "/c", "start", exe, conn.LocalAddr().String()) + if err := cmd.Start(); err != nil { + t.Fatalf("Start failed: %v", err) + } + + // read pid + var data [512]byte + n, _, err := conn.ReadFrom(data[:]) + if err != nil { + t.Fatalf("ReadFrom failed: %v", err) + } + + // gracefully kill pid + err = exec.Command("taskkill.exe", "/pid", string(data[:n])).Run() + if err != nil { + t.Fatalf("failed to kill: %v", err) + } + + // check received signal + n, _, err = conn.ReadFrom(data[:]) + if err != nil { + t.Fatalf("ReadFrom failed: %v", err) + } + expected := syscall.SIGTERM.String() + if n != len(expected) && string(data[:len(expected)]) != expected { + t.Fatalf("Expected '%s' got: %s", expected, data[:n]) + } + + if err := cmd.Wait(); err != nil { + t.Fatalf("Program exited with error: %v", err) + } +} + // TestLibraryCtrlHandler tests that Go DLL allows calling program to handle console control events. // See https://golang.org/issues/35965. func TestLibraryCtrlHandler(t *testing.T) { diff --git a/src/runtime/testdata/testwinsignal/main.go b/src/runtime/testdata/testwinsignal/main.go new file mode 100644 index 00000000000000..96c979b70ee3b2 --- /dev/null +++ b/src/runtime/testdata/testwinsignal/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "net" + "os" + "os/signal" + "strconv" + "time" +) + +func main() { + c := make(chan os.Signal, 1) + signal.Notify(c) + + con, _ := net.Dial("udp", os.Args[1]) + con.Write([]byte(strconv.Itoa(os.Getpid()))) + sig := <-c + + time.Sleep(time.Second) + con.Write([]byte(sig.String())) +} From 03abc2d8188b1f9a6c1dc3b275ff55e6d272e048 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Sat, 10 Oct 2020 02:05:51 +0100 Subject: [PATCH 03/11] Block indefinitely only for irrecoverable conditions. --- src/runtime/os_windows.go | 6 +++++- src/runtime/signal_windows_test.go | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go index 0805322baf0542..66ada0c75405b0 100644 --- a/src/runtime/os_windows.go +++ b/src/runtime/os_windows.go @@ -1010,7 +1010,11 @@ func ctrlhandler1(_type uint32) uint32 { } if sigsend(s) { - stdcall1(_Sleep, uintptr(_INFINITE)) + if s == _SIGTERM { + // Windows terminates the process after this handler returns. + // Block indefinitely to give signal handlers a chance to clean up. + stdcall1(_Sleep, uintptr(_INFINITE)) + } return 1 } return 0 diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index 07293502ca3822..c5475bcf5e5a45 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -82,6 +82,8 @@ func sendCtrlBreak(pid int) error { return nil } +// TestCtrlHandler tests that Go can gracefully handle closing the console window. +// See https://golang.org/issues/41884. func TestCtrlHandler(t *testing.T) { if *flagQuick { t.Skip("-quick") From 54ffa75ccb13cd2c1fc4b7c8a7382a943425d798 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Sun, 3 Jan 2021 14:32:27 +0000 Subject: [PATCH 04/11] Ensure test exits cleanly before being killed. --- src/runtime/signal_windows_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index 4c1854ae40866e..95aa05b4a52ccf 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -5,6 +5,7 @@ package runtime_test import ( "bufio" "bytes" + "context" "fmt" "internal/testenv" "io/ioutil" @@ -114,8 +115,11 @@ func TestCtrlHandler(t *testing.T) { defer conn.Close() conn.SetDeadline(time.Now().Add(5 * time.Second)) + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) + defer cancel() + // run test program - cmd = exec.Command("cmd.exe", "/c", "start", exe, conn.LocalAddr().String()) + cmd = exec.CommandContext(ctx, "cmd.exe", "/c", "start", exe, conn.LocalAddr().String()) if err := cmd.Start(); err != nil { t.Fatalf("Start failed: %v", err) } From 487707365e3e0a659fcc6052aa7f9294d62affc1 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Sun, 3 Jan 2021 15:02:12 +0000 Subject: [PATCH 05/11] Fix test. --- src/runtime/signal_windows_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index 95aa05b4a52ccf..ab2a30b2bf0cb5 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -143,7 +143,7 @@ func TestCtrlHandler(t *testing.T) { t.Fatalf("ReadFrom failed: %v", err) } expected := syscall.SIGTERM.String() - if n != len(expected) && string(data[:len(expected)]) != expected { + if string(data[:n]) != expected { t.Fatalf("Expected '%s' got: %s", expected, data[:n]) } From b52e8999ba06e80810f2e70deae4c2457ca44b4f Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Mon, 4 Jan 2021 13:20:16 +0000 Subject: [PATCH 06/11] Wait for child. --- src/runtime/signal_windows_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index ab2a30b2bf0cb5..5988a5696bd5b0 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -119,7 +119,7 @@ func TestCtrlHandler(t *testing.T) { defer cancel() // run test program - cmd = exec.CommandContext(ctx, "cmd.exe", "/c", "start", exe, conn.LocalAddr().String()) + cmd = exec.CommandContext(ctx, "cmd.exe", "/c", "start", "", "/wait", exe, conn.LocalAddr().String()) if err := cmd.Start(); err != nil { t.Fatalf("Start failed: %v", err) } From 084270ef1807192ea847542aae32034cf08cb462 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Mon, 4 Jan 2021 13:34:03 +0000 Subject: [PATCH 07/11] Review changes. --- src/runtime/signal_windows_test.go | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index 5988a5696bd5b0..23298b76ae3231 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "internal/testenv" - "io/ioutil" "net" "os" "os/exec" @@ -86,21 +85,10 @@ func sendCtrlBreak(pid int) error { // TestCtrlHandler tests that Go can gracefully handle closing the console window. // See https://golang.org/issues/41884. func TestCtrlHandler(t *testing.T) { - if *flagQuick { - t.Skip("-quick") - } testenv.MustHaveGoBuild(t) - testenv.MustHaveExecPath(t, "gcc") - testprog.Lock() - defer testprog.Unlock() - dir, err := ioutil.TempDir("", "go-build") - if err != nil { - t.Fatalf("failed to create temp directory: %v", err) - } - defer os.RemoveAll(dir) // build go program - exe := filepath.Join(dir, "test.exe") + exe := filepath.Join(t.TempDir(), "test.exe") cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", exe, "testdata/testwinsignal/main.go") out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() if err != nil { @@ -142,9 +130,8 @@ func TestCtrlHandler(t *testing.T) { if err != nil { t.Fatalf("ReadFrom failed: %v", err) } - expected := syscall.SIGTERM.String() - if string(data[:n]) != expected { - t.Fatalf("Expected '%s' got: %s", expected, data[:n]) + if expected, got := syscall.SIGTERM.String(), string(data[:n]); expected != got { + t.Fatalf("Expected '%s' got: %s", expected, got) } if err := cmd.Wait(); err != nil { From 56e2ba08bf58cee67661ba5abaf8f505e9d2d695 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 6 Jan 2021 13:18:48 +0000 Subject: [PATCH 08/11] Review comments. --- src/runtime/signal_windows_test.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index 23298b76ae3231..073e59030b0cb6 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -86,6 +86,7 @@ func sendCtrlBreak(pid int) error { // See https://golang.org/issues/41884. func TestCtrlHandler(t *testing.T) { testenv.MustHaveGoBuild(t) + t.Parallel() // build go program exe := filepath.Join(t.TempDir(), "test.exe") @@ -106,26 +107,28 @@ func TestCtrlHandler(t *testing.T) { ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) defer cancel() - // run test program - cmd = exec.CommandContext(ctx, "cmd.exe", "/c", "start", "", "/wait", exe, conn.LocalAddr().String()) + // run test program, in a new command window + cmd = exec.CommandContext(ctx, "cmd.exe", "/c", "start", "Test Command Window", "/wait", exe, conn.LocalAddr().String()) if err := cmd.Start(); err != nil { t.Fatalf("Start failed: %v", err) } - // read pid + // read pid of the test program + // cmd.Process.Pid is the pid of cmd.exe, not test.exe + // also ensures the test program is ready to receive signals var data [512]byte n, _, err := conn.ReadFrom(data[:]) if err != nil { t.Fatalf("ReadFrom failed: %v", err) } - // gracefully kill pid + // gracefully kill pid, this closes the command window err = exec.Command("taskkill.exe", "/pid", string(data[:n])).Run() if err != nil { t.Fatalf("failed to kill: %v", err) } - // check received signal + // check child received, handled SIGTERM n, _, err = conn.ReadFrom(data[:]) if err != nil { t.Fatalf("ReadFrom failed: %v", err) @@ -134,6 +137,7 @@ func TestCtrlHandler(t *testing.T) { t.Fatalf("Expected '%s' got: %s", expected, got) } + // check child exited gracefully (exit code 0, didn't timeout) if err := cmd.Wait(); err != nil { t.Fatalf("Program exited with error: %v", err) } From 4977a6a278aae93cf85af84c7f047c3e28c45687 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 6 Jan 2021 15:17:48 +0000 Subject: [PATCH 09/11] Use CREATE_NEW_CONSOLE. --- src/runtime/signal_windows_test.go | 80 ++++++++++++---------- src/runtime/testdata/testwinsignal/main.go | 8 +-- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index 073e59030b0cb6..6a66299fd46479 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -5,18 +5,16 @@ package runtime_test import ( "bufio" "bytes" - "context" "fmt" "internal/testenv" - "net" "os" "os/exec" "path/filepath" "runtime" + "strconv" "strings" "syscall" "testing" - "time" ) func TestVectoredHandlerDontCrashOnLibrary(t *testing.T) { @@ -96,50 +94,60 @@ func TestCtrlHandler(t *testing.T) { t.Fatalf("failed to build go exe: %v\n%s", err, out) } - // udp socket for synchronization - conn, err := net.ListenPacket("udp", "[::1]:0") + // run test program + cmd = exec.Command(exe) + var stderr bytes.Buffer + cmd.Stderr = &stderr + outPipe, err := cmd.StdoutPipe() if err != nil { - t.Fatalf("ListenPacket failed: %v", err) + t.Fatalf("Failed to create stdout pipe: %v", err) } - defer conn.Close() - conn.SetDeadline(time.Now().Add(5 * time.Second)) - - ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) - defer cancel() + outReader := bufio.NewReader(outPipe) - // run test program, in a new command window - cmd = exec.CommandContext(ctx, "cmd.exe", "/c", "start", "Test Command Window", "/wait", exe, conn.LocalAddr().String()) + // in a new command window + const CREATE_NEW_CONSOLE = 0x00000010 + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: CREATE_NEW_CONSOLE, + } if err := cmd.Start(); err != nil { t.Fatalf("Start failed: %v", err) } - // read pid of the test program - // cmd.Process.Pid is the pid of cmd.exe, not test.exe - // also ensures the test program is ready to receive signals - var data [512]byte - n, _, err := conn.ReadFrom(data[:]) - if err != nil { - t.Fatalf("ReadFrom failed: %v", err) - } + errCh := make(chan error, 1) + go func() { + // wait for child to be ready to receive signals + if line, err := outReader.ReadString('\n'); err != nil { + errCh <- fmt.Errorf("could not read stdout: %w", err) + return + } else if strings.TrimSpace(line) != "ready" { + errCh <- fmt.Errorf("unexpected message: %v", line) + return + } - // gracefully kill pid, this closes the command window - err = exec.Command("taskkill.exe", "/pid", string(data[:n])).Run() - if err != nil { - t.Fatalf("failed to kill: %v", err) - } + // gracefully kill pid, this closes the command window + if err := exec.Command("taskkill.exe", "/pid", strconv.Itoa(cmd.Process.Pid)).Run(); err != nil { + errCh <- fmt.Errorf("failed to kill: %w", err) + return + } - // check child received, handled SIGTERM - n, _, err = conn.ReadFrom(data[:]) - if err != nil { - t.Fatalf("ReadFrom failed: %v", err) - } - if expected, got := syscall.SIGTERM.String(), string(data[:n]); expected != got { - t.Fatalf("Expected '%s' got: %s", expected, got) - } + // check child received, handled SIGTERM + if line, err := outReader.ReadString('\n'); err != nil { + errCh <- fmt.Errorf("could not read stdout: %w", err) + return + } else if expected, got := syscall.SIGTERM.String(), strings.TrimSpace(line); expected != got { + errCh <- fmt.Errorf("Expected '%s' got: %s", expected, got) + return + } + + errCh <- nil + }() - // check child exited gracefully (exit code 0, didn't timeout) + if err := <-errCh; err != nil { + t.Fatal(err) + } + // check child exited gracefully, did not timeout if err := cmd.Wait(); err != nil { - t.Fatalf("Program exited with error: %v", err) + t.Fatalf("Program exited with error: %v\n%s", err, &stderr) } } diff --git a/src/runtime/testdata/testwinsignal/main.go b/src/runtime/testdata/testwinsignal/main.go index 96c979b70ee3b2..d8cd884ffac638 100644 --- a/src/runtime/testdata/testwinsignal/main.go +++ b/src/runtime/testdata/testwinsignal/main.go @@ -1,10 +1,9 @@ package main import ( - "net" + "fmt" "os" "os/signal" - "strconv" "time" ) @@ -12,10 +11,9 @@ func main() { c := make(chan os.Signal, 1) signal.Notify(c) - con, _ := net.Dial("udp", os.Args[1]) - con.Write([]byte(strconv.Itoa(os.Getpid()))) + fmt.Println("ready") sig := <-c time.Sleep(time.Second) - con.Write([]byte(sig.String())) + fmt.Println(sig) } From 43b589f06538eed0dd9893e431f9bb35df895b09 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Tue, 12 Jan 2021 17:57:26 +0000 Subject: [PATCH 10/11] Review comments. --- src/runtime/signal_windows_test.go | 53 +++++++++++++----------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index 6a66299fd46479..1dbf9628a13706 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -105,46 +105,37 @@ func TestCtrlHandler(t *testing.T) { outReader := bufio.NewReader(outPipe) // in a new command window - const CREATE_NEW_CONSOLE = 0x00000010 + const _CREATE_NEW_CONSOLE = 0x00000010 cmd.SysProcAttr = &syscall.SysProcAttr{ - CreationFlags: CREATE_NEW_CONSOLE, + CreationFlags: _CREATE_NEW_CONSOLE, } if err := cmd.Start(); err != nil { t.Fatalf("Start failed: %v", err) } + defer func() { + cmd.Process.Kill() + cmd.Wait() + }() - errCh := make(chan error, 1) - go func() { - // wait for child to be ready to receive signals - if line, err := outReader.ReadString('\n'); err != nil { - errCh <- fmt.Errorf("could not read stdout: %w", err) - return - } else if strings.TrimSpace(line) != "ready" { - errCh <- fmt.Errorf("unexpected message: %v", line) - return - } - - // gracefully kill pid, this closes the command window - if err := exec.Command("taskkill.exe", "/pid", strconv.Itoa(cmd.Process.Pid)).Run(); err != nil { - errCh <- fmt.Errorf("failed to kill: %w", err) - return - } - - // check child received, handled SIGTERM - if line, err := outReader.ReadString('\n'); err != nil { - errCh <- fmt.Errorf("could not read stdout: %w", err) - return - } else if expected, got := syscall.SIGTERM.String(), strings.TrimSpace(line); expected != got { - errCh <- fmt.Errorf("Expected '%s' got: %s", expected, got) - return - } + // wait for child to be ready to receive signals + if line, err := outReader.ReadString('\n'); err != nil { + t.Fatalf("could not read stdout: %v", err) + } else if strings.TrimSpace(line) != "ready" { + t.Fatalf("unexpected message: %s", line) + } - errCh <- nil - }() + // gracefully kill pid, this closes the command window + if err := exec.Command("taskkill.exe", "/pid", strconv.Itoa(cmd.Process.Pid)).Run(); err != nil { + t.Fatalf("failed to kill: %v", err) + } - if err := <-errCh; err != nil { - t.Fatal(err) + // check child received, handled SIGTERM + if line, err := outReader.ReadString('\n'); err != nil { + t.Fatalf("could not read stdout: %v", err) + } else if expected, got := syscall.SIGTERM.String(), strings.TrimSpace(line); expected != got { + t.Fatalf("Expected '%s' got: %s", expected, got) } + // check child exited gracefully, did not timeout if err := cmd.Wait(); err != nil { t.Fatalf("Program exited with error: %v\n%s", err, &stderr) From 4ddb2d776e169b761e436fb4459e5ea6921a561c Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 27 Jan 2021 18:45:57 +0000 Subject: [PATCH 11/11] Review comments. --- src/runtime/signal_windows_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/runtime/signal_windows_test.go b/src/runtime/signal_windows_test.go index 1dbf9628a13706..33a9b92ee73f1f 100644 --- a/src/runtime/signal_windows_test.go +++ b/src/runtime/signal_windows_test.go @@ -108,6 +108,7 @@ func TestCtrlHandler(t *testing.T) { const _CREATE_NEW_CONSOLE = 0x00000010 cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: _CREATE_NEW_CONSOLE, + HideWindow: true, } if err := cmd.Start(); err != nil { t.Fatalf("Start failed: %v", err)