From da33b5a7864216827da8b105afb24837f73d634a Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Fri, 18 Jan 2019 08:50:59 -0800 Subject: [PATCH] Add Windows support (#144) Currently the e2e tests are tagged as `nowindows`. We will create release binaries for windows once those are passing. --- .bazelci/presubmit.yml | 19 +++ e2e/common.go | 13 +- e2e/ibazel.go | 5 +- e2e/live_reload/BUILD | 1 + e2e/output_runner/BUILD | 1 + e2e/profiler/BUILD | 1 + e2e/simple/BUILD | 1 + ibazel/BUILD | 2 + ibazel/command/BUILD | 6 +- ibazel/command/command.go | 15 +- ibazel/command/command_test.go | 3 +- ibazel/command/default_command.go | 23 +-- ibazel/command/default_command_test.go | 45 ++++-- ibazel/command/notify_command.go | 27 ++-- ibazel/command/notify_command_test.go | 14 +- ibazel/main.go | 22 --- ibazel/main_unix.go | 40 +++++ ibazel/main_windows.go | 19 +++ ibazel/process_group/BUILD | 30 ++++ ibazel/process_group/process_group.go | 41 +++++ ibazel/process_group/process_group_unix.go | 58 +++++++ ibazel/process_group/process_group_windows.go | 153 ++++++++++++++++++ ibazel/process_group/syscalls_windows.go | 79 +++++++++ release/npm/BUILD | 3 + 24 files changed, 529 insertions(+), 92 deletions(-) create mode 100644 ibazel/main_unix.go create mode 100644 ibazel/main_windows.go create mode 100644 ibazel/process_group/BUILD create mode 100644 ibazel/process_group/process_group.go create mode 100644 ibazel/process_group/process_group_unix.go create mode 100644 ibazel/process_group/process_group_windows.go create mode 100644 ibazel/process_group/syscalls_windows.go diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 16f5a300..8623c4fb 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -1,23 +1,42 @@ --- platforms: ubuntu1404: + build_flags: + - "--build_tag_filters=-nolinux" build_targets: - "..." test_flags: - "--features=race" + - "--test_tag_filters=-nolinux" test_targets: - "..." ubuntu1604: + build_flags: + - "--build_tag_filters=-nolinux" build_targets: - "..." test_flags: - "--features=race" + - "--test_tag_filters=-nolinux" test_targets: - "..." macos: + build_flags: + - "--build_tag_filters=-nomacos" build_targets: - "..." test_flags: - "--features=race" + - "--test_tag_filters=-nomacos" + test_targets: + - "..." + windows: + build_flags: + - "--build_tag_filters=-nowindows" + build_targets: + - "..." + test_flags: + - "--test_tag_filters=-nowindows" + - "--experimental_enable_runfiles" test_targets: - "..." diff --git a/e2e/common.go b/e2e/common.go index ad24b1bf..1bacb722 100644 --- a/e2e/common.go +++ b/e2e/common.go @@ -22,12 +22,13 @@ func GetPath(p string) string { return path } -var ibazelPath string +var ibazelPath = getiBazelPath() -func init() { - var err error - ibazelPath = GetPath(fmt.Sprintf("ibazel/%s_%s_pure_stripped/ibazel", runtime.GOOS, runtime.GOARCH)) - if err != nil { - panic(err) +func getiBazelPath() string { + suffix := "" + // Windows expects executables to end in .exe + if runtime.GOOS == "windows" { + suffix = ".exe" } + return GetPath(fmt.Sprintf("ibazel/%s_%s_pure_stripped/ibazel%s", runtime.GOOS, runtime.GOARCH, suffix)) } diff --git a/e2e/ibazel.go b/e2e/ibazel.go index f9ba5c16..73820c11 100644 --- a/e2e/ibazel.go +++ b/e2e/ibazel.go @@ -158,10 +158,7 @@ func (i *IBazelTester) build(target string, additionalArgs []string) { } func (i *IBazelTester) run(target string, additionalArgs []string) { - args := []string{ - "--bazel_path=" + i.bazelPath(), - "--log_to_file=/tmp/ibazel_output.log", - } + args := []string{"--bazel_path=" + i.bazelPath()} args = append(args, additionalArgs...) args = append(args, "run") args = append(args, target) diff --git a/e2e/live_reload/BUILD b/e2e/live_reload/BUILD index 6966964c..474672c6 100644 --- a/e2e/live_reload/BUILD +++ b/e2e/live_reload/BUILD @@ -10,6 +10,7 @@ bazel_go_integration_test( "//ibazel", ], importpath = "github.com/bazelbuild/bazel-watcher/e2e/live_reload", + tags = ["nowindows"], versions = GET_LATEST_BAZEL_VERSIONS(), deps = [ "//e2e:go_default_library", diff --git a/e2e/output_runner/BUILD b/e2e/output_runner/BUILD index 8af109d9..e12d939b 100644 --- a/e2e/output_runner/BUILD +++ b/e2e/output_runner/BUILD @@ -8,6 +8,7 @@ bazel_go_integration_test( "//ibazel", ], importpath = "github.com/bazelbuild/bazel-watcher/e2e/output_runner", + tags = ["nowindows"], versions = GET_LATEST_BAZEL_VERSIONS(), deps = [ "//e2e:go_default_library", diff --git a/e2e/profiler/BUILD b/e2e/profiler/BUILD index 2d789225..a485317b 100644 --- a/e2e/profiler/BUILD +++ b/e2e/profiler/BUILD @@ -10,6 +10,7 @@ bazel_go_integration_test( "//ibazel", ], importpath = "github.com/bazelbuild/bazel-watcher/e2e/profiler", + tags = ["nowindows"], versions = GET_LATEST_BAZEL_VERSIONS(), deps = [ "//e2e:go_default_library", diff --git a/e2e/simple/BUILD b/e2e/simple/BUILD index db5c8a51..c96ef8c4 100644 --- a/e2e/simple/BUILD +++ b/e2e/simple/BUILD @@ -10,6 +10,7 @@ bazel_go_integration_test( "//ibazel", ], importpath = "github.com/bazelbuild/bazel-watcher/e2e/simple", + tags = ["nowindows"], versions = GET_LATEST_BAZEL_VERSIONS(), deps = [ "//e2e:go_default_library", diff --git a/ibazel/BUILD b/ibazel/BUILD index 9bebb387..3448a95b 100644 --- a/ibazel/BUILD +++ b/ibazel/BUILD @@ -34,6 +34,8 @@ go_library( "ibazel.go", "lifecycle.go", "main.go", + "main_unix.go", + "main_windows.go", "source_event_handler.go", ], importpath = "github.com/bazelbuild/bazel-watcher/ibazel", diff --git a/ibazel/command/BUILD b/ibazel/command/BUILD index f9cd8f17..964e3c99 100644 --- a/ibazel/command/BUILD +++ b/ibazel/command/BUILD @@ -23,7 +23,10 @@ go_library( ], importpath = "github.com/bazelbuild/bazel-watcher/ibazel/command", visibility = ["//ibazel:__subpackages__"], - deps = ["//bazel:go_default_library"], + deps = [ + "//bazel:go_default_library", + "//ibazel/process_group:go_default_library", + ], ) go_test( @@ -39,5 +42,6 @@ go_test( deps = [ "//bazel:go_default_library", "//bazel/testing:go_default_library", + "//ibazel/process_group:go_default_library", ], ) diff --git a/ibazel/command/command.go b/ibazel/command/command.go index f4d4b6c2..3f75bbbf 100644 --- a/ibazel/command/command.go +++ b/ibazel/command/command.go @@ -22,12 +22,12 @@ import ( "os/exec" "runtime" "strings" - "syscall" "github.com/bazelbuild/bazel-watcher/bazel" + "github.com/bazelbuild/bazel-watcher/ibazel/process_group" ) -var execCommand = exec.Command +var execCommand = process_group.Command var bazelNew = bazel.New // Command is an object that wraps the logic of running a task in Bazel and @@ -41,9 +41,9 @@ type Command interface { // start will be called by most implementations since this logic is extremely // common. -func start(b bazel.Bazel, target string, args []string) (*bytes.Buffer, *exec.Cmd) { +func start(b bazel.Bazel, target string, args []string) (*bytes.Buffer, process_group.ProcessGroup) { var filePattern strings.Builder - filePattern.WriteString("bazel_script_path") + filePattern.WriteString("bazel_script_path*") if runtime.GOOS == "windows" { filePattern.WriteString(".bat") } @@ -65,11 +65,8 @@ func start(b bazel.Bazel, target string, args []string) (*bytes.Buffer, *exec.Cm // Now that we have built the target, construct a executable form of it for // execution in a go routine. cmd := execCommand(runScriptPath, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - // Set a process group id (PGID) on the subprocess. This is - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.RootProcess().Stdout = os.Stdout + cmd.RootProcess().Stderr = os.Stderr return outputBuffer, cmd } diff --git a/ibazel/command/command_test.go b/ibazel/command/command_test.go index 2a32d4c1..c9974d9b 100644 --- a/ibazel/command/command_test.go +++ b/ibazel/command/command_test.go @@ -19,6 +19,7 @@ import ( "testing" "github.com/bazelbuild/bazel-watcher/bazel" + "github.com/bazelbuild/bazel-watcher/ibazel/process_group" ) var oldExecCommand = execCommand @@ -36,7 +37,7 @@ func assertKilled(t *testing.T, cmd *exec.Cmd) { } func TestSubprocessRunning(t *testing.T) { - execCommand = func(name string, args ...string) *exec.Cmd { + execCommand = func(name string, args ...string) process_group.ProcessGroup { return oldExecCommand("ls") // Every system has ls. } defer func() { execCommand = oldExecCommand }() diff --git a/ibazel/command/default_command.go b/ibazel/command/default_command.go index 01861be4..599e3319 100644 --- a/ibazel/command/default_command.go +++ b/ibazel/command/default_command.go @@ -18,15 +18,15 @@ import ( "bytes" "fmt" "os" - "os/exec" - "syscall" + + "github.com/bazelbuild/bazel-watcher/ibazel/process_group" ) type defaultCommand struct { target string bazelArgs []string args []string - cmd *exec.Cmd + pg process_group.ProcessGroup } // DefaultCommand is the normal mode of interacting with iBazel. If you start a @@ -41,7 +41,7 @@ func DefaultCommand(bazelArgs []string, target string, args []string) Command { } func (c *defaultCommand) Terminate() { - if !subprocessRunning(c.cmd) { + if c.pg != nil && !subprocessRunning(c.pg.RootProcess()) { return } @@ -50,9 +50,10 @@ func (c *defaultCommand) Terminate() { // send to the PGID, send the signal to the negative of the process PID. // Normally I would do this by calling c.cmd.Process.Signal, but that // only goes to the PID not the PGID. - syscall.Kill(-c.cmd.Process.Pid, syscall.SIGKILL) - c.cmd.Wait() - c.cmd = nil + c.pg.Kill() + c.pg.Wait() + c.pg.Close() + c.pg = nil } func (c *defaultCommand) Start() (*bytes.Buffer, error) { @@ -63,12 +64,12 @@ func (c *defaultCommand) Start() (*bytes.Buffer, error) { b.WriteToStdout(true) var outputBuffer *bytes.Buffer - outputBuffer, c.cmd = start(b, c.target, c.args) + outputBuffer, c.pg = start(b, c.target, c.args) - c.cmd.Env = os.Environ() + c.pg.RootProcess().Env = os.Environ() var err error - if err = c.cmd.Start(); err != nil { + if err = c.pg.Start(); err != nil { fmt.Fprintf(os.Stderr, "Error starting process: %v\n", err) return outputBuffer, err } @@ -83,5 +84,5 @@ func (c *defaultCommand) NotifyOfChanges() *bytes.Buffer { } func (c *defaultCommand) IsSubprocessRunning() bool { - return subprocessRunning(c.cmd) + return c.pg != nil && subprocessRunning(c.pg.RootProcess()) } diff --git a/ibazel/command/default_command_test.go b/ibazel/command/default_command_test.go index 02a0dacc..1402b6b7 100644 --- a/ibazel/command/default_command_test.go +++ b/ibazel/command/default_command_test.go @@ -16,18 +16,28 @@ package command import ( "os" - "os/exec" - "syscall" + "runtime" "testing" mock_bazel "github.com/bazelbuild/bazel-watcher/bazel/testing" + "github.com/bazelbuild/bazel-watcher/ibazel/process_group" ) func TestDefaultCommand(t *testing.T) { - toKill := exec.Command("sleep", "10s") - toKill.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + var toKill process_group.ProcessGroup - execCommand = func(name string, args ...string) *exec.Cmd { + if runtime.GOOS == "windows" { + // TODO(jchw): Remove hardcoded path. + toKill = process_group.Command("C:\\windows\\system32\\notepad") + } else { + toKill = process_group.Command("sleep", "10s") + } + + execCommand = func(name string, args ...string) process_group.ProcessGroup { + if runtime.GOOS == "windows" { + // TODO(jchw): Remove hardcoded path. + return oldExecCommand("C:\\windows\\system32\\where") + } return oldExecCommand("ls") // Every system has ls. } defer func() { execCommand = oldExecCommand }() @@ -35,46 +45,47 @@ func TestDefaultCommand(t *testing.T) { c := &defaultCommand{ args: []string{"moo"}, bazelArgs: []string{}, - cmd: toKill, + pg: toKill, target: "//path/to:target", } if c.IsSubprocessRunning() { - t.Errorf("New subprocess shouldn't have been started yet. State: %v", toKill.ProcessState) + t.Errorf("New subprocess shouldn't have been started yet. State: %v", toKill.RootProcess().ProcessState) } toKill.Start() if !c.IsSubprocessRunning() { - t.Errorf("New subprocess was never started. State: %v", toKill.ProcessState) + t.Errorf("New subprocess was never started. State: %v", toKill.RootProcess().ProcessState) } // This is synonymous with killing the job so use it to kill the job and test everything. c.NotifyOfChanges() - assertKilled(t, toKill) + assertKilled(t, toKill.RootProcess()) } func TestDefaultCommand_Start(t *testing.T) { // Set up mock execCommand and prep it to be returned - execCommand = func(name string, args ...string) *exec.Cmd { + execCommand = func(name string, args ...string) process_group.ProcessGroup { + if runtime.GOOS == "windows" { + // TODO(jchw): Remove hardcoded path. + return oldExecCommand("C:\\windows\\system32\\where") + } return oldExecCommand("ls") // Every system has ls. } defer func() { execCommand = oldExecCommand }() b := &mock_bazel.MockBazel{} - _, cmd := start(b, "//path/to:target", []string{"moo"}) - cmd.Start() + _, pg := start(b, "//path/to:target", []string{"moo"}) + pg.Start() - if cmd.Stdout != os.Stdout { + if pg.RootProcess().Stdout != os.Stdout { t.Errorf("Didn't set Stdout correctly") } - if cmd.Stderr != os.Stderr { + if pg.RootProcess().Stderr != os.Stderr { t.Errorf("Didn't set Stderr correctly") } - if cmd.SysProcAttr.Setpgid != true { - t.Errorf("Never set PGID (will prevent killing process trees -- see notes in ibazel.go") - } b.AssertActions(t, [][]string{ []string{"Run", "--script_path=.*", "//path/to:target"}, diff --git a/ibazel/command/notify_command.go b/ibazel/command/notify_command.go index 0dbc0265..fa7527ff 100644 --- a/ibazel/command/notify_command.go +++ b/ibazel/command/notify_command.go @@ -19,8 +19,8 @@ import ( "fmt" "io" "os" - "os/exec" - "syscall" + + "github.com/bazelbuild/bazel-watcher/ibazel/process_group" ) type notifyCommand struct { @@ -28,7 +28,7 @@ type notifyCommand struct { bazelArgs []string args []string - cmd *exec.Cmd + pg process_group.ProcessGroup stdin io.WriteCloser } @@ -43,7 +43,7 @@ func NotifyCommand(bazelArgs []string, target string, args []string) Command { } func (c *notifyCommand) Terminate() { - if !subprocessRunning(c.cmd) { + if c.pg != nil && !subprocessRunning(c.pg.RootProcess()) { return } @@ -52,9 +52,10 @@ func (c *notifyCommand) Terminate() { // send to the PGID, send the signal to the negative of the process PID. // Normally I would do this by calling c.cmd.Process.Signal, but that // only goes to the PID not the PGID. - syscall.Kill(-c.cmd.Process.Pid, syscall.SIGKILL) - c.cmd.Wait() - c.cmd = nil + c.pg.Kill() + c.pg.Wait() + c.pg.Close() + c.pg = nil } func (c *notifyCommand) Start() (*bytes.Buffer, error) { @@ -65,18 +66,18 @@ func (c *notifyCommand) Start() (*bytes.Buffer, error) { b.WriteToStdout(true) var outputBuffer *bytes.Buffer - outputBuffer, c.cmd = start(b, c.target, c.args) + outputBuffer, c.pg = start(b, c.target, c.args) // Keep the writer around. var err error - c.stdin, err = c.cmd.StdinPipe() + c.stdin, err = c.pg.RootProcess().StdinPipe() if err != nil { fmt.Fprintf(os.Stderr, "Error getting stdin pipe: %v\n", err) return outputBuffer, err } - c.cmd.Env = append(os.Environ(), "IBAZEL_NOTIFY_CHANGES=y") + c.pg.RootProcess().Env = append(os.Environ(), "IBAZEL_NOTIFY_CHANGES=y") - if err = c.cmd.Start(); err != nil { + if err = c.pg.Start(); err != nil { fmt.Fprintf(os.Stderr, "Error starting process: %v\n", err) return outputBuffer, err } @@ -96,7 +97,7 @@ func (c *notifyCommand) NotifyOfChanges() *bytes.Buffer { fmt.Fprintf(os.Stderr, "FAILURE: %v\n", res) _, err := c.stdin.Write([]byte("IBAZEL_BUILD_COMPLETED FAILURE\n")) if err != nil { - fmt.Fprintf(os.Stderr, "Error writing failure to stdin: %s\n%v", err) + fmt.Fprintf(os.Stderr, "Error writing failure to stdin: %s\n", err) } } else { fmt.Fprintf(os.Stderr, "SUCCESS\n") @@ -109,5 +110,5 @@ func (c *notifyCommand) NotifyOfChanges() *bytes.Buffer { } func (c *notifyCommand) IsSubprocessRunning() bool { - return subprocessRunning(c.cmd) + return c.pg != nil && subprocessRunning(c.pg.RootProcess()) } diff --git a/ibazel/command/notify_command_test.go b/ibazel/command/notify_command_test.go index 88d6f6d5..86ed3303 100644 --- a/ibazel/command/notify_command_test.go +++ b/ibazel/command/notify_command_test.go @@ -16,31 +16,29 @@ package command import ( "errors" - "os/exec" - "syscall" "testing" "github.com/bazelbuild/bazel-watcher/bazel" mock_bazel "github.com/bazelbuild/bazel-watcher/bazel/testing" + "github.com/bazelbuild/bazel-watcher/ibazel/process_group" ) func TestNotifyCommand(t *testing.T) { - cmd := exec.Command("cat") - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + pg := process_group.Command("cat") c := ¬ifyCommand{ args: []string{"moo"}, bazelArgs: []string{}, - cmd: cmd, + pg: pg, target: "//path/to:target", } if c.IsSubprocessRunning() { - t.Errorf("New subprocess shouldn't have been started yet. State: %v", cmd.ProcessState) + t.Errorf("New subprocess shouldn't have been started yet. State: %v", pg.RootProcess().ProcessState) } var err error - c.stdin, err = cmd.StdinPipe() + c.stdin, err = pg.RootProcess().StdinPipe() if err != nil { t.Error(err) } @@ -74,7 +72,7 @@ func TestNotifyCommand(t *testing.T) { t.Error(err) } - out, err := cmd.CombinedOutput() + out, err := pg.CombinedOutput() if err != nil { t.Error(err) } diff --git a/ibazel/main.go b/ibazel/main.go index 4ae5f954..06afea9e 100644 --- a/ibazel/main.go +++ b/ibazel/main.go @@ -19,7 +19,6 @@ import ( "fmt" "os" "strings" - "syscall" "time" ) @@ -163,24 +162,3 @@ func handle(i *IBazel, command string, args []string) { return } } - -func setUlimit() error { - var lim syscall.Rlimit - - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim) - if err != nil { - return err - } - - // set the "soft" file descriptor to the maximum - // allowed by a userspace program. - // http://man7.org/linux/man-pages/man2/getrlimit.2.html - lim.Cur = lim.Max - - err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &lim) - if err != nil { - return err - } - - return nil -} diff --git a/ibazel/main_unix.go b/ibazel/main_unix.go new file mode 100644 index 00000000..7fc04594 --- /dev/null +++ b/ibazel/main_unix.go @@ -0,0 +1,40 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !windows + +package main + +import "syscall" + +func setUlimit() error { + var lim syscall.Rlimit + + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim) + if err != nil { + return err + } + + // set the "soft" file descriptor to the maximum + // allowed by a userspace program. + // http://man7.org/linux/man-pages/man2/getrlimit.2.html + lim.Cur = lim.Max + + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &lim) + if err != nil { + return err + } + + return nil +} diff --git a/ibazel/main_windows.go b/ibazel/main_windows.go new file mode 100644 index 00000000..210dc9cb --- /dev/null +++ b/ibazel/main_windows.go @@ -0,0 +1,19 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +func setUlimit() error { + return nil +} diff --git a/ibazel/process_group/BUILD b/ibazel/process_group/BUILD new file mode 100644 index 00000000..4327bbeb --- /dev/null +++ b/ibazel/process_group/BUILD @@ -0,0 +1,30 @@ +# Copyright 2018 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "process_group.go", + "process_group_unix.go", + "process_group_windows.go", + "syscalls_windows.go", + ], + importpath = "github.com/bazelbuild/bazel-watcher/ibazel/process_group", + visibility = ["//ibazel:__subpackages__"], + deps = [ + "//ibazel/workspace_finder:go_default_library", + ], +) diff --git a/ibazel/process_group/process_group.go b/ibazel/process_group/process_group.go new file mode 100644 index 00000000..02021328 --- /dev/null +++ b/ibazel/process_group/process_group.go @@ -0,0 +1,41 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Implements a platform-independent process group. In effect, this allows you +// to terminate an entire tree of processes in one go. On Linux, this uses the +// Process Group system. On Windows, this uses Job Objects. +// +// Most of the things you would normally do with an exec.Cmd are safe to do +// with the RootProcess() Cmd, with two exceptions: +// +// - You cannot call .Start() on it. Use ProcessGroup.Start() instead. Doing +// so will work on Linux but not Windows. +// - You should not change .SysProcAttr. + +package process_group + +import ( + "os/exec" +) + +// ProcessGroup represents a tree of processes that can be terminated +// simultaneously. +type ProcessGroup interface { + RootProcess() *exec.Cmd + Start() error + Kill() error + Wait() error + Close() error + CombinedOutput() ([]byte, error) +} diff --git a/ibazel/process_group/process_group_unix.go b/ibazel/process_group/process_group_unix.go new file mode 100644 index 00000000..8b098e1e --- /dev/null +++ b/ibazel/process_group/process_group_unix.go @@ -0,0 +1,58 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !windows + +package process_group + +import ( + "os/exec" + "syscall" +) + +type unixProcessGroup struct { + root *exec.Cmd +} + +// Command creates a new ProcessGroup with a root command specified by the +// arguments. +func Command(name string, arg ...string) ProcessGroup { + root := exec.Command(name, arg...) + root.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + return &unixProcessGroup{root} +} + +func (pg *unixProcessGroup) RootProcess() *exec.Cmd { + return pg.root +} + +func (pg *unixProcessGroup) Start() error { + return pg.root.Start() +} + +func (pg *unixProcessGroup) Kill() error { + return syscall.Kill(-pg.root.Process.Pid, syscall.SIGKILL) +} + +func (pg *unixProcessGroup) Wait() error { + return pg.root.Wait() +} + +func (pg *unixProcessGroup) Close() error { + return nil +} + +func (pg *unixProcessGroup) CombinedOutput() ([]byte, error) { + return pg.root.CombinedOutput() +} diff --git a/ibazel/process_group/process_group_windows.go b/ibazel/process_group/process_group_windows.go new file mode 100644 index 00000000..c49c5298 --- /dev/null +++ b/ibazel/process_group/process_group_windows.go @@ -0,0 +1,153 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package process_group + +import ( + "bytes" + "errors" + "log" + "os/exec" + "syscall" + "unsafe" +) + +type winProcessGroup struct { + root *exec.Cmd + job syscall.Handle + ioport syscall.Handle +} + +// Command creates a new ProcessGroup with a root command specified by the +// arguments. +func Command(name string, arg ...string) ProcessGroup { + root := exec.Command(name, arg...) + root.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createSuspended} + return &winProcessGroup{root, syscall.Handle(0), syscall.Handle(0)} +} + +func (pg *winProcessGroup) RootProcess() *exec.Cmd { + return pg.root +} + +func (pg *winProcessGroup) Start() error { + if pg.job != 0 { + return errors.New("job already started") + } + + err := pg.root.Start() + if err != nil { + return err + } + + pg.job, err = createJobObject() + if err != nil { + return err + } + + pg.ioport, err = syscall.CreateIoCompletionPort(syscall.InvalidHandle, syscall.Handle(0), 0, 1) + if err != nil { + return err + } + + port := jobObjectAssociationCompletionPort{ + CompletionKey: pg.job, + CompletionPort: pg.ioport, + } + + err = setInformationJobObject(pg.job, jobObjectAssociateCompletionPortInformation, uintptr(unsafe.Pointer(&port)), unsafe.Sizeof(port)) + if err != nil { + return err + } + + process, err := syscall.OpenProcess(processAllAccess, false, uint32(pg.root.Process.Pid)) + if err != nil { + return err + } + + err = assignProcessToJobObject(pg.job, process) + if err != nil { + return err + } + + err = ntResumeProcess(process) + if err != nil { + return err + } + + syscall.CloseHandle(process) + + return nil +} + +func (pg *winProcessGroup) Kill() error { + log.Println("Kill()") + if pg.job == 0 { + return errors.New("job not started") + } + + err := terminateJobObject(pg.job, 0) + if err != nil { + return err + } + + return nil +} + +func (pg *winProcessGroup) Wait() error { + var code uint32 + var key uint32 + var op *syscall.Overlapped + + for { + err := syscall.GetQueuedCompletionStatus(pg.ioport, &code, &key, &op, syscall.INFINITE) + if err != nil { + return err + } + if key == uint32(pg.job) && code == jobObjectMsgActiveProcessZero { + break + } + } + + return nil +} + +func (pg *winProcessGroup) Close() error { + err := syscall.CloseHandle(pg.job) + if err != nil { + return err + } + + err = syscall.CloseHandle(pg.ioport) + if err != nil { + return err + } + + return nil +} + +func (pg *winProcessGroup) Run() error { + if err := pg.Start(); err != nil { + return err + } + return pg.Wait() +} + +func (pg *winProcessGroup) CombinedOutput() ([]byte, error) { + var b bytes.Buffer + pg.root.Stdout = &b + pg.root.Stderr = &b + err := pg.Run() + return b.Bytes(), err +} diff --git a/ibazel/process_group/syscalls_windows.go b/ibazel/process_group/syscalls_windows.go new file mode 100644 index 00000000..210babfd --- /dev/null +++ b/ibazel/process_group/syscalls_windows.go @@ -0,0 +1,79 @@ +package process_group + +import ( + "syscall" +) + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + modntdll = syscall.NewLazyDLL("ntdll.dll") + + procCreateJobObject = modkernel32.NewProc("CreateJobObjectW") + procSetInformationJobObject = modkernel32.NewProc("SetInformationJobObject") + procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject") + procTerminateJobObject = modkernel32.NewProc("TerminateJobObject") + procNtResumeProcess = modntdll.NewProc("NtResumeProcess") +) + +const ( + // CREATE_SUSPEND flag - used in CreateProcess 'dwCreationFlags' field to + // specify that the process should start suspended. + createSuspended = 0x00000004 + + // PROCESS_ALL_ACCESS flag - used in OpenProcess 'dwDesiredAccess' field to + // request all permission. Seems needed for NtResumeProcess. + processAllAccess = 0x1F0FFF + + // JobObjectAssociateCompletionPortInformation - used in + // SetInformationJobObject to set completion port of job object. + jobObjectAssociateCompletionPortInformation = 7 + + // JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO - message returned from IO completion + // port when no more processes are active in job object. + jobObjectMsgActiveProcessZero = 4 +) + +type jobObjectAssociationCompletionPort struct { + CompletionKey syscall.Handle + CompletionPort syscall.Handle +} + +func createJobObject() (syscall.Handle, error) { + job, _, errno := syscall.Syscall(procCreateJobObject.Addr(), 2, 0, 0, 0) + if errno != 0 { + return 0, errno + } + return syscall.Handle(job), nil +} + +func setInformationJobObject(job syscall.Handle, infoClass int, objInfo uintptr, objInfoLen uintptr) error { + _, _, errno := syscall.Syscall6(procSetInformationJobObject.Addr(), 4, uintptr(job), uintptr(infoClass), objInfo, objInfoLen, 0, 0) + if errno != 0 { + return errno + } + return nil +} + +func assignProcessToJobObject(job syscall.Handle, process syscall.Handle) error { + _, _, errno := syscall.Syscall(procAssignProcessToJobObject.Addr(), 2, uintptr(job), uintptr(process), 0) + if errno != 0 { + return errno + } + return nil +} + +func terminateJobObject(job syscall.Handle, exitCode uint) error { + _, _, errno := syscall.Syscall(procTerminateJobObject.Addr(), 2, uintptr(job), uintptr(exitCode), 0) + if errno != 0 { + return errno + } + return nil +} + +func ntResumeProcess(process syscall.Handle) error { + _, _, errno := syscall.Syscall(procNtResumeProcess.Addr(), 1, uintptr(process), 0, 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/release/npm/BUILD b/release/npm/BUILD index 7cd7b5e7..eacaadb1 100644 --- a/release/npm/BUILD +++ b/release/npm/BUILD @@ -22,6 +22,9 @@ genrule( outs = ["package.json"], cmd = "$(location :npm) $(location //:CONTRIBUTORS) > $@", stamp = 1, + tags = [ + "nowindows", + ], tools = [ ":npm", ],