diff --git a/bazel/bazel.go b/bazel/bazel.go index 38c64cb9..ab896b5c 100644 --- a/bazel/bazel.go +++ b/bazel/bazel.go @@ -15,10 +15,12 @@ package bazel import ( + "bytes" "context" "errors" "flag" "fmt" + "io" "os" "os/exec" "strings" @@ -35,9 +37,9 @@ type Bazel interface { WriteToStdout(v bool) Info() (map[string]string, error) Query(args ...string) (*blaze_query.QueryResult, error) - Build(args ...string) error - Test(args ...string) error - Run(args ...string) (*exec.Cmd, error) + Build(args ...string) (*bytes.Buffer, error) + Test(args ...string) (*bytes.Buffer, error) + Run(args ...string) (*exec.Cmd, *bytes.Buffer, error) Wait() error Cancel() } @@ -69,17 +71,38 @@ func (b *bazel) WriteToStdout(v bool) { b.writeToStdout = v } -func (b *bazel) newCommand(command string, args ...string) { +func (b *bazel) newCommand(command string, args ...string) (*bytes.Buffer, *bytes.Buffer) { b.ctx, b.cancel = context.WithCancel(context.Background()) args = append([]string{command}, args...) + + if b.writeToStderr || b.writeToStdout { + containsColor := false + for _, arg := range args { + if strings.HasPrefix(arg, "--color") { + containsColor = true + } + } + if !containsColor { + args = append(args, "--color=yes") + } + } b.cmd = exec.CommandContext(b.ctx, *bazelPath, args...) + + stdoutBuffer := new(bytes.Buffer) + stderrBuffer := new(bytes.Buffer) if b.writeToStderr { - b.cmd.Stderr = os.Stderr + b.cmd.Stderr = io.MultiWriter(os.Stderr, stdoutBuffer) + } else { + b.cmd.Stderr = stdoutBuffer } if b.writeToStdout { - b.cmd.Stdout = os.Stdout + b.cmd.Stdout = io.MultiWriter(os.Stdout, stderrBuffer) + } else { + b.cmd.Stdout = stderrBuffer } + + return stdoutBuffer, stderrBuffer } // Displays information about the state of the bazel process in the @@ -99,13 +122,13 @@ func (b *bazel) newCommand(command string, args ...string) { func (b *bazel) Info() (map[string]string, error) { b.WriteToStderr(false) b.WriteToStdout(false) - b.newCommand("info") + stdoutBuffer, _ := b.newCommand("info") - info, err := b.cmd.Output() + err := b.cmd.Run() if err != nil { return nil, err } - return b.processInfo(string(info)) + return b.processInfo(stdoutBuffer.String()) } func (b *bazel) processInfo(info string) (map[string]string, error) { @@ -139,58 +162,62 @@ func (b *bazel) processInfo(info string) (map[string]string, error) { // // res, err := b.Query('somepath(//path/to/package:target, //dependency)') func (b *bazel) Query(args ...string) (*blaze_query.QueryResult, error) { - blazeArgs := append([]string(nil), "--output=proto", "--order_output=no") + blazeArgs := append([]string(nil), "--output=proto", "--order_output=no", "--color=no") blazeArgs = append(blazeArgs, args...) - b.WriteToStderr(true) - b.WriteToStdout(false) - b.newCommand("query", blazeArgs...) + b.WriteToStderr(true) + b.WriteToStdout(false) + _, stderrBuffer := b.newCommand("query", blazeArgs...) + + err := b.cmd.Run() - out, err := b.cmd.Output() if err != nil { return nil, err } - return b.processQuery(out) + return b.processQuery(stderrBuffer.Bytes()) } func (b *bazel) processQuery(out []byte) (*blaze_query.QueryResult, error) { var qr blaze_query.QueryResult if err := proto.Unmarshal(out, &qr); err != nil { - fmt.Printf("Could not read blaze query response: %s %s", err, out) + fmt.Fprintf(os.Stderr, "Could not read blaze query response. Error: %s\nOutput: %s\n", err, out) return nil, err } + return &qr, nil } -func (b *bazel) Build(args ...string) error { - b.newCommand("build", append(b.args, args...)...) - +func (b *bazel) Build(args ...string) (*bytes.Buffer, error) { + stdoutBuffer, stderrBuffer := b.newCommand("build", append(b.args, args...)...) err := b.cmd.Run() - return err + _, _= stdoutBuffer.Write(stderrBuffer.Bytes()) + return stdoutBuffer, err } -func (b *bazel) Test(args ...string) error { - b.newCommand("test", append(b.args, args...)...) - +func (b *bazel) Test(args ...string) (*bytes.Buffer, error) { + stdoutBuffer, stderrBuffer := b.newCommand("test", append(b.args, args...)...) err := b.cmd.Run() - return err + _, _ = stdoutBuffer.Write(stderrBuffer.Bytes()) + return stdoutBuffer, err } // Build the specified target (singular) and run it with the given arguments. -func (b *bazel) Run(args ...string) (*exec.Cmd, error) { +func (b *bazel) Run(args ...string) (*exec.Cmd, *bytes.Buffer, error) { b.WriteToStderr(true) - b.WriteToStdout(true) - b.newCommand("run", args...) + b.WriteToStdout(true) + stdoutBuffer, stderrBuffer := b.newCommand("run", args...) b.cmd.Stdin = os.Stdin + _, _ = stdoutBuffer.Write(stderrBuffer.Bytes()) + err := b.cmd.Run() if err != nil { - return nil, err + return nil, stdoutBuffer, err } - return b.cmd, err + return b.cmd, stdoutBuffer, err } func (b *bazel) Wait() error { diff --git a/bazel/bazel_test.go b/bazel/bazel_test.go index b5470214..0f26ac30 100644 --- a/bazel/bazel_test.go +++ b/bazel/bazel_test.go @@ -15,6 +15,8 @@ package bazel import ( + "bytes" + "io" "os" "reflect" "testing" @@ -49,36 +51,38 @@ KEY3: value`) func TestWriteToStderrAndStdout(t *testing.T) { b := &bazel{} + stdoutBuffer := new(bytes.Buffer) + stderrBuffer := new(bytes.Buffer) // By default it should write to its own pipe. b.newCommand("version") - if b.cmd.Stdout == os.Stdout { - t.Errorf("Set stdout to os.Stdout") + if reflect.DeepEqual(b.cmd.Stdout, io.MultiWriter(os.Stdout, stderrBuffer)) { + t.Errorf("Set stdout to os.Stdout and stderrBuffer") } - if b.cmd.Stderr == os.Stderr { - t.Errorf("Set stderr to os.Stderr") + if reflect.DeepEqual(b.cmd.Stderr, io.MultiWriter(os.Stderr, stdoutBuffer)) { + t.Errorf("Set stderr to os.Stderr and stdoutBuffer") } // If set to true it should write to the os version b.WriteToStderr(true) b.WriteToStdout(true) b.newCommand("version") - if b.cmd.Stdout != os.Stdout { - t.Errorf("Didn't set stdout to os.Stdout") + if !reflect.DeepEqual(b.cmd.Stdout, io.MultiWriter(os.Stdout, stderrBuffer)) { + t.Errorf("Didn't set stdout to os.Stdout and stderrBuffer") } - if b.cmd.Stderr != os.Stderr { - t.Errorf("Didn't set stderr to os.Stderr") + if !reflect.DeepEqual(b.cmd.Stderr, io.MultiWriter(os.Stderr, stdoutBuffer)) { + t.Errorf("Didn't set stderr to os.Stderr and stdoutBuffer") } // If set to false it should not write to the os version b.WriteToStderr(false) b.WriteToStdout(false) b.newCommand("version") - if b.cmd.Stdout == os.Stdout { - t.Errorf("Set stdout to os.Stdout") + if reflect.DeepEqual(b.cmd.Stdout, io.MultiWriter(os.Stdout, stderrBuffer)) { + t.Errorf("Set stdout to os.Stdout and stderrBuffer") } - if b.cmd.Stderr == os.Stderr { - t.Errorf("Set stderr to os.Stderr") + if reflect.DeepEqual(b.cmd.Stderr, io.MultiWriter(os.Stderr, stdoutBuffer)) { + t.Errorf("Set stderr to os.Stderr and stdoutBuffer") } } diff --git a/bazel/testing/mock.go b/bazel/testing/mock.go index 419c7ecf..f2272f10 100644 --- a/bazel/testing/mock.go +++ b/bazel/testing/mock.go @@ -15,6 +15,7 @@ package testing import ( + "bytes" "os/exec" "regexp" "testing" @@ -62,20 +63,20 @@ func (b *MockBazel) Query(args ...string) (*blaze_query.QueryResult, error) { return res, nil } -func (b *MockBazel) Build(args ...string) error { +func (b *MockBazel) Build(args ...string) (*bytes.Buffer, error) { b.actions = append(b.actions, append([]string{"Build"}, args...)) - return b.buildError + return nil, b.buildError } func (b *MockBazel) BuildError(e error) { b.buildError = e } -func (b *MockBazel) Test(args ...string) error { +func (b *MockBazel) Test(args ...string) (*bytes.Buffer, error) { b.actions = append(b.actions, append([]string{"Test"}, args...)) - return nil + return nil, nil } -func (b *MockBazel) Run(args ...string) (*exec.Cmd, error) { +func (b *MockBazel) Run(args ...string) (*exec.Cmd, *bytes.Buffer, error) { b.actions = append(b.actions, append([]string{"Run"}, args...)) - return nil, nil + return nil, nil, nil } func (b *MockBazel) WaitError(e error) { b.waitError = e diff --git a/ibazel/BUILD b/ibazel/BUILD index 7f35e616..6316435e 100644 --- a/ibazel/BUILD +++ b/ibazel/BUILD @@ -34,15 +34,16 @@ go_library( "lifecycle.go", "main.go", "source_event_handler.go", - "workspace_finder.go" ], importpath = "github.com/bazelbuild/bazel-watcher/ibazel", visibility = ["//visibility:private"], deps = [ "//bazel:go_default_library", "//ibazel/command:go_default_library", + "//ibazel/output_runner:go_default_library", "//ibazel/profiler:go_default_library", "//ibazel/live_reload:go_default_library", + "//ibazel/workspace_finder:go_default_library", "//third_party/bazel/master/src/main/protobuf:go_default_library", "@com_github_fsnotify_fsnotify//:go_default_library", ], diff --git a/ibazel/command/command.go b/ibazel/command/command.go index 5532699a..38e24d6c 100644 --- a/ibazel/command/command.go +++ b/ibazel/command/command.go @@ -15,6 +15,7 @@ package command import ( + "bytes" "fmt" "io/ioutil" "os" @@ -30,15 +31,15 @@ var bazelNew = bazel.New // Command is an object that wraps the logic of running a task in Bazel and // manipulating it. type Command interface { - Start() error + Start() (*bytes.Buffer, error) Terminate() - NotifyOfChanges() + NotifyOfChanges() *bytes.Buffer IsSubprocessRunning() bool } // start will be called by most implementations since this logic is extremely // common. -func start(b bazel.Bazel, target string, args []string) *exec.Cmd { +func start(b bazel.Bazel, target string, args []string) (*bytes.Buffer, *exec.Cmd) { tmpfile, err := ioutil.TempFile("", "bazel_script_path") if err != nil { fmt.Print(err) @@ -49,7 +50,7 @@ func start(b bazel.Bazel, target string, args []string) *exec.Cmd { } // Start by building the binary - b.Run("--script_path="+tmpfile.Name(), target) + _, outputBuffer, _ := b.Run("--script_path="+tmpfile.Name(), target) runScriptPath := tmpfile.Name() @@ -62,7 +63,7 @@ func start(b bazel.Bazel, target string, args []string) *exec.Cmd { // Set a process group id (PGID) on the subprocess. This is cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - return cmd + return outputBuffer, cmd } func subprocessRunning(cmd *exec.Cmd) bool { diff --git a/ibazel/command/default_command.go b/ibazel/command/default_command.go index a4be4693..be6ed0d7 100644 --- a/ibazel/command/default_command.go +++ b/ibazel/command/default_command.go @@ -15,6 +15,7 @@ package command import ( + "bytes" "fmt" "os" "os/exec" @@ -54,29 +55,31 @@ func (c *defaultCommand) Terminate() { c.cmd = nil } -func (c *defaultCommand) Start() error { +func (c *defaultCommand) Start() (*bytes.Buffer, error) { b := bazelNew() b.SetArguments(c.bazelArgs) b.WriteToStderr(true) b.WriteToStdout(true) - c.cmd = start(b, c.target, c.args) + outputBuffer, cmd := start(b, c.target, c.args) + c.cmd = cmd c.cmd.Env = os.Environ() var err error if err = c.cmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "Error starting process: %v\n", err) - return err + return outputBuffer, err } fmt.Fprintf(os.Stderr, "Starting...") - return nil + return outputBuffer, nil } -func (c *defaultCommand) NotifyOfChanges() { +func (c *defaultCommand) NotifyOfChanges() *bytes.Buffer { c.Terminate() c.Start() + return nil } func (c *defaultCommand) IsSubprocessRunning() bool { diff --git a/ibazel/command/default_command_test.go b/ibazel/command/default_command_test.go index 26253279..02a0dacc 100644 --- a/ibazel/command/default_command_test.go +++ b/ibazel/command/default_command_test.go @@ -63,7 +63,7 @@ func TestDefaultCommand_Start(t *testing.T) { b := &mock_bazel.MockBazel{} - cmd := start(b, "//path/to:target", []string{"moo"}) + _, cmd := start(b, "//path/to:target", []string{"moo"}) cmd.Start() if cmd.Stdout != os.Stdout { diff --git a/ibazel/command/notify_command.go b/ibazel/command/notify_command.go index 4f649d11..4f1b1e16 100644 --- a/ibazel/command/notify_command.go +++ b/ibazel/command/notify_command.go @@ -15,6 +15,7 @@ package command import ( + "bytes" "fmt" "io" "os" @@ -56,40 +57,41 @@ func (c *notifyCommand) Terminate() { c.cmd = nil } -func (c *notifyCommand) Start() error { +func (c *notifyCommand) Start() (*bytes.Buffer, error) { b := bazelNew() b.SetArguments(c.bazelArgs) b.WriteToStderr(true) b.WriteToStdout(true) - c.cmd = start(b, c.target, c.args) + outputBuffer, cmd := start(b, c.target, c.args) + c.cmd = cmd // Keep the writer around. var err error c.stdin, err = c.cmd.StdinPipe() if err != nil { fmt.Fprintf(os.Stderr, "Error getting stdin pipe: %v\n", err) - return err + return outputBuffer, err } c.cmd.Env = append(os.Environ(), "IBAZEL_NOTIFY_CHANGES=y") if err = c.cmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "Error starting process: %v\n", err) - return err + return outputBuffer, err } fmt.Fprintf(os.Stderr, "Starting...") - return nil + return outputBuffer, nil } -func (c *notifyCommand) NotifyOfChanges() { +func (c *notifyCommand) NotifyOfChanges() *bytes.Buffer { b := bazelNew() b.SetArguments(c.bazelArgs) b.WriteToStderr(true) b.WriteToStdout(true) - res := b.Build(c.target) + outputBuffer, res := b.Build(c.target) if res != nil { fmt.Fprintf(os.Stderr, "FAILURE: %v\n", res) _, err := c.stdin.Write([]byte("IBAZEL_BUILD_COMPLETED FAILURE\n")) @@ -103,6 +105,7 @@ func (c *notifyCommand) NotifyOfChanges() { fmt.Fprintf(os.Stderr, "Error writing success to stdin: %v\n", err) } } + return outputBuffer } func (c *notifyCommand) IsSubprocessRunning() bool { diff --git a/ibazel/ibazel.go b/ibazel/ibazel.go index 464c231f..9bb53c59 100644 --- a/ibazel/ibazel.go +++ b/ibazel/ibazel.go @@ -15,6 +15,7 @@ package main import ( + "bytes" "errors" "fmt" "os" @@ -27,7 +28,9 @@ import ( "github.com/bazelbuild/bazel-watcher/bazel" "github.com/bazelbuild/bazel-watcher/ibazel/command" "github.com/bazelbuild/bazel-watcher/ibazel/live_reload" + "github.com/bazelbuild/bazel-watcher/ibazel/output_runner" "github.com/bazelbuild/bazel-watcher/ibazel/profiler" + "github.com/bazelbuild/bazel-watcher/ibazel/workspace_finder" "github.com/fsnotify/fsnotify" blaze_query "github.com/bazelbuild/bazel-watcher/third_party/bazel/master/src/main/protobuf" @@ -39,7 +42,7 @@ var commandDefaultCommand = command.DefaultCommand var commandNotifyCommand = command.NotifyCommand type State string -type runnableCommand func(...string) error +type runnableCommand func(...string) (*bytes.Buffer, error) const ( DEBOUNCE_QUERY State = "DEBOUNCE_QUERY" @@ -63,7 +66,7 @@ type IBazel struct { sigs chan os.Signal // Signals channel for the current process interruptCount int - workspaceFinder WorkspaceFinder + workspaceFinder workspace_finder.WorkspaceFinder buildFileWatcher *fsnotify.Watcher sourceFileWatcher *fsnotify.Watcher @@ -85,19 +88,21 @@ func New() (*IBazel, error) { i.debounceDuration = 100 * time.Millisecond i.filesWatched = map[*fsnotify.Watcher]map[string]bool{} - i.workspaceFinder = &MainWorkspaceFinder{} + i.workspaceFinder = &workspace_finder.MainWorkspaceFinder{} i.sigs = make(chan os.Signal, 1) - signal.Notify(i.sigs, syscall.SIGINT, syscall.SIGTERM) + signal.Notify(i.sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) liveReload := live_reload.New() profiler := profiler.New(Version) + outputRunner := output_runner.New() liveReload.AddEventsListener(profiler) i.lifecycleListeners = []Lifecycle{ liveReload, profiler, + outputRunner, } info, _ := i.getInfo() @@ -115,7 +120,7 @@ func New() (*IBazel, error) { } func (i *IBazel) handleSignals() { - // Got an OS signal (SIGINT, SIGTERM). + // Got an OS signal (SIGINT, SIGTERM, SIGHUP). sig := <-i.sigs switch sig { @@ -134,6 +139,13 @@ func (i *IBazel) handleSignals() { } osExit(3) return + case syscall.SIGHUP: + if i.cmd != nil && i.cmd.IsSubprocessRunning() { + fmt.Fprintf(os.Stderr, "\nSubprocess killed from getting SIGHUP\n") + i.cmd.Terminate() + } + osExit(3) + return default: fmt.Fprintf(os.Stderr, "Got a signal that wasn't handled. Please file a bug against bazel-watcher that describes how you did this. This is a big problem.\n") } @@ -197,9 +209,9 @@ func (i *IBazel) beforeCommand(targets []string, command string) { } } -func (i *IBazel) afterCommand(targets []string, command string, success bool) { +func (i *IBazel) afterCommand(targets []string, command string, success bool, output *bytes.Buffer) { for _, l := range i.lifecycleListeners { - l.AfterCommand(targets, command, success) + l.AfterCommand(targets, command, success, output) } } @@ -300,38 +312,38 @@ func (i *IBazel) iteration(command string, commandToRun runnableCommand, targets case RUN: fmt.Fprintf(os.Stderr, "%sing %s\n", strings.Title(command), joinedTargets) i.beforeCommand(targets, command) - err := commandToRun(targets...) - i.afterCommand(targets, command, err == nil) + outputBuffer, err := commandToRun(targets...) + i.afterCommand(targets, command, err == nil, outputBuffer) i.state = WAIT } } -func (i *IBazel) build(targets ...string) error { +func (i *IBazel) build(targets ...string) (*bytes.Buffer, error) { b := i.newBazel() b.Cancel() b.WriteToStderr(true) b.WriteToStdout(true) - err := b.Build(targets...) + outputBuffer, err := b.Build(targets...) if err != nil { fmt.Fprintf(os.Stderr, "Build error: %v\n", err) - return err + return outputBuffer, err } - return nil + return outputBuffer, nil } -func (i *IBazel) test(targets ...string) error { +func (i *IBazel) test(targets ...string) (*bytes.Buffer, error) { b := i.newBazel() b.Cancel() b.WriteToStderr(true) b.WriteToStdout(true) - err := b.Test(targets...) + outputBuffer, err := b.Test(targets...) if err != nil { fmt.Fprintf(os.Stderr, "Build error: %v\n", err) - return err + return outputBuffer, err } - return nil + return outputBuffer, err } func contains(l []string, e string) bool { @@ -368,21 +380,21 @@ func (i *IBazel) setupRun(target string) command.Command { } } -func (i *IBazel) run(targets ...string) error { +func (i *IBazel) run(targets ...string) (*bytes.Buffer, error) { if i.cmd == nil { // If the command is empty, we are in our first pass through the state // machine and we need to make a command object. i.cmd = i.setupRun(targets[0]) - err := i.cmd.Start() + outputBuffer, err := i.cmd.Start() if err != nil { fmt.Fprintf(os.Stderr, "Run start failed %v\n", err) } - return err + return outputBuffer, err } fmt.Fprintf(os.Stderr, "Notifying of changes\n") - i.cmd.NotifyOfChanges() - return nil + outputBuffer := i.cmd.NotifyOfChanges() + return outputBuffer, nil } func (i *IBazel) queryRule(rule string) (*blaze_query.Rule, error) { diff --git a/ibazel/ibazel_test.go b/ibazel/ibazel_test.go index 981848d7..98fcf9e3 100644 --- a/ibazel/ibazel_test.go +++ b/ibazel/ibazel_test.go @@ -15,6 +15,7 @@ package main import ( + "bytes" "fmt" "os" "reflect" @@ -26,6 +27,7 @@ import ( "github.com/bazelbuild/bazel-watcher/bazel" mock_bazel "github.com/bazelbuild/bazel-watcher/bazel/testing" "github.com/bazelbuild/bazel-watcher/ibazel/command" + "github.com/bazelbuild/bazel-watcher/ibazel/workspace_finder" "github.com/fsnotify/fsnotify" blaze_query "github.com/bazelbuild/bazel-watcher/third_party/bazel/master/src/main/protobuf" @@ -51,15 +53,16 @@ type mockCommand struct { terminated bool } -func (m *mockCommand) Start() error { +func (m *mockCommand) Start() (*bytes.Buffer, error) { if m.started { panic("Can't run command twice") } m.started = true - return nil + return nil, nil } -func (m *mockCommand) NotifyOfChanges() { +func (m *mockCommand) NotifyOfChanges() *bytes.Buffer { m.notifiedOfChanges = true + return nil } func (m *mockCommand) Terminate() { if !m.started { @@ -124,7 +127,7 @@ func newIBazel(t *testing.T) *IBazel { t.Errorf("Error creating IBazel: %s", err) } - i.workspaceFinder = &FakeWorkspaceFinder{} + i.workspaceFinder = &workspace_finder.FakeWorkspaceFinder{} return i } @@ -154,9 +157,9 @@ func TestIBazelLoop(t *testing.T) { // First let's consume all the events from all the channels we care about called := false - command := func(targets ...string) error { + command := func(targets ...string) (*bytes.Buffer, error) { called = true - return nil + return nil, nil } i.state = QUERY diff --git a/ibazel/lifecycle.go b/ibazel/lifecycle.go index 80521aff..c2cf1aea 100644 --- a/ibazel/lifecycle.go +++ b/ibazel/lifecycle.go @@ -1,6 +1,8 @@ package main import ( + "bytes" + "github.com/bazelbuild/bazel-watcher/third_party/bazel/master/src/main/protobuf" ) @@ -29,5 +31,5 @@ type Lifecycle interface { // AfterCommand is called after a blaze $COMMAND is run with the result of // that command. // command: "build"|"test"|"run" - AfterCommand(targets []string, command string, success bool) + AfterCommand(targets []string, command string, success bool, output *bytes.Buffer) } diff --git a/ibazel/live_reload/server.go b/ibazel/live_reload/server.go index c97770f5..e4ca283b 100644 --- a/ibazel/live_reload/server.go +++ b/ibazel/live_reload/server.go @@ -15,6 +15,7 @@ package live_reload import ( + "bytes" "flag" "fmt" "log" @@ -71,7 +72,7 @@ func (l *LiveReloadServer) ChangeDetected(targets []string, changeType string, c func (l *LiveReloadServer) BeforeCommand(targets []string, command string) {} -func (l *LiveReloadServer) AfterCommand(targets []string, command string, success bool) { +func (l *LiveReloadServer) AfterCommand(targets []string, command string, success bool, output *bytes.Buffer) { l.triggerReload(targets) } diff --git a/ibazel/main.go b/ibazel/main.go index db7a395e..52b8f25b 100644 --- a/ibazel/main.go +++ b/ibazel/main.go @@ -28,6 +28,7 @@ var Version = "Development" var overrideableBazelFlags []string = []string{ "--test_output=", "--config=", + "--curses=no", } var debounceDuration = flag.Duration("debounce", 100*time.Millisecond, "Debounce duration") @@ -114,7 +115,7 @@ func main() { i, err := New() if err != nil { - fmt.Fprintf(os.Stderr, "Error creating iBazel", err) + fmt.Fprintf(os.Stderr, "Error creating iBazel: %s\n", err) os.Exit(1) } i.SetDebounceDuration(*debounceDuration) diff --git a/ibazel/output_runner/BUILD b/ibazel/output_runner/BUILD new file mode 100644 index 00000000..f1e7c8a2 --- /dev/null +++ b/ibazel/output_runner/BUILD @@ -0,0 +1,47 @@ +# 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 = [ + "output_runner.go", + ], + importpath = "github.com/bazelbuild/bazel-watcher/ibazel/output_runner", + visibility = ["//ibazel:__subpackages__"], + deps = [ + "//bazel:go_default_library", + "//third_party/bazel/master/src/main/protobuf:go_default_library", + "//ibazel/workspace_finder:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "output_runner_test.go", + ], + data = ["output_runner_test.json"], + embed = [":go_default_library"], + importpath = "github.com/bazelbuild/bazel-watcher/ibazel", + deps = [ + "//bazel:go_default_library", + "//bazel/testing:go_default_library", + "//ibazel/command:go_default_library", + "//third_party/bazel/master/src/main/protobuf:go_default_library", + "@com_github_fsnotify_fsnotify//:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + ], +) diff --git a/ibazel/output_runner/output_runner.go b/ibazel/output_runner/output_runner.go new file mode 100644 index 00000000..15636bf0 --- /dev/null +++ b/ibazel/output_runner/output_runner.go @@ -0,0 +1,189 @@ +// 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 output_runner + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "os/exec" + "regexp" + "strings" + "strconv" + + "github.com/bazelbuild/bazel-watcher/ibazel/workspace_finder" + blaze_query "github.com/bazelbuild/bazel-watcher/third_party/bazel/master/src/main/protobuf" +) + +var runOutput = flag.Bool( + "run_output", + false, + "Search for commands in Bazel output that match a regex and execute them, the default path of file should be in the workspace root .bazel_fix_commands.json") +var runOutputInteractive = flag.Bool( + "run_output_interactive", + true, + "Use an interactive prompt when executing commands in Bazel output") + +type OutputRunner struct{} + +type Optcmd struct { + Regex string `json:"regex"` + Command string `json:"command"` + Args []string `json:"args"` +} + +func New() *OutputRunner { + i := &OutputRunner{} + return i +} + +func (i *OutputRunner) Initialize(info *map[string]string) {} + +func (i *OutputRunner) TargetDecider(rule *blaze_query.Rule) {} + +func (i *OutputRunner) ChangeDetected(targets []string, changeType string, change string) {} + +func (i *OutputRunner) BeforeCommand(targets []string, command string) {} + +func (i *OutputRunner) AfterCommand(targets []string, command string, success bool, output *bytes.Buffer) { + if *runOutput == false || output == nil { + return + } + + jsonCommandPath := ".bazel_fix_commands.json" + defaultRegex := Optcmd{ + Regex: "^buildozer '(.*)'\\s+(.*)$", + Command: "buildozer", + Args: []string{"$1", "$2"}, + } + + optcmd := readConfigs(jsonCommandPath) + if optcmd == nil { + fmt.Fprintf(os.Stderr, "Use default regex\n") + optcmd = []Optcmd{defaultRegex} + } + commandLines, commands, args := matchRegex(optcmd, output) + for idx, _ := range commandLines { + if *runOutputInteractive { + if promptCommand(commandLines[idx]) { + executeCommand(commands[idx], args[idx]) + } + } else { + executeCommand(commands[idx], args[idx]) + } + } +} + +func readConfigs(configPath string) []Optcmd { + jsonFile, err := os.Open(configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err) + return nil + } + defer jsonFile.Close() + + byteValue, _ := ioutil.ReadAll(jsonFile) + var optcmd []Optcmd + json.Unmarshal(byteValue, &optcmd) + + return optcmd +} + +func matchRegex(optcmd []Optcmd, output *bytes.Buffer) ([]string, []string, [][]string) { + var commandLines, commands []string + var args [][]string + scanner := bufio.NewScanner(output) + for scanner.Scan() { + line := scanner.Text() + for _, oc := range optcmd { + re := regexp.MustCompile(oc.Regex) + matches := re.FindStringSubmatch(line) + if matches != nil && len(matches) >= 3 { + commandLines = append(commandLines, matches[0]) + commands = append(commands, convertArg(matches, oc.Command)) + args = append(args, convertArgs(matches, oc.Args)) + } + } + } + return commandLines, commands, args +} + +func convertArg(matches []string, arg string) string { + if strings.HasPrefix(arg, "$") { + val, _ := strconv.Atoi(arg[1:]) + return matches[val] + } + return arg +} + +func convertArgs(matches []string, args []string) []string { + var rst []string + for i, _ := range args { + if strings.HasPrefix(args[i], "$") { + val, _ := strconv.Atoi(args[i][1:]) + rst = append(rst, matches[val]) + } else { + rst = append(rst, args[i]) + } + } + return rst +} + +func promptCommand(command string) bool { + reader := bufio.NewReader(os.Stdin) + fmt.Fprintf(os.Stderr, "Do you want to execute this command?\n%s\n[y/N]", command) + text, _ := reader.ReadString('\n') + text = strings.ToLower(text) + text = strings.TrimSpace(text) + text = strings.TrimRight(text, "\n") + if text == "y" { + return true + } else { + return false + } +} + +func executeCommand(command string, args []string) { + for i, arg := range args { + args[i] = strings.TrimSpace(arg) + } + fmt.Fprintf(os.Stderr, "Executing command: %s\n", command) + workspaceFinder := &workspace_finder.MainWorkspaceFinder{} + workspacePath, err := workspaceFinder.FindWorkspace() + if err != nil { + fmt.Fprintf(os.Stderr, "Error finding workspace: %v\n", err) + os.Exit(5) + } + fmt.Fprintf(os.Stderr, "Workspace path: %s\n", workspacePath) + + ctx, _ := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, command, args...) + fmt.Fprintf(os.Stderr, "Executing command: %s %s\n", cmd.Path, strings.Join(cmd.Args, ",")) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Dir = workspacePath + + err = cmd.Run() + if err != nil { + fmt.Fprintf(os.Stderr, "Command failed: %s %s. Error: %s\n", command, args, err) + } +} + +func (i *OutputRunner) Cleanup() {} diff --git a/ibazel/output_runner/output_runner_test.go b/ibazel/output_runner/output_runner_test.go new file mode 100644 index 00000000..93c56058 --- /dev/null +++ b/ibazel/output_runner/output_runner_test.go @@ -0,0 +1,117 @@ +// 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 output_runner + +import ( + "bytes" + "reflect" + "testing" +) + +func TestConvertArgs(t *testing.T) { + matches := []string{"my_command my_arg1 my_arg2 my_arg3", "my_command", "my_arg1", "my_arg2", "my_arg3"} + // Command parsing tests + for _, c := range []struct { + cmd string + truth string + }{ + {"$1", "my_command"}, + {"warning", "warning"}, + {"keep_command", "keep_command"}, + } { + new_cmd := convertArg(matches, c.cmd) + if !reflect.DeepEqual(c.truth, new_cmd) { + t.Errorf("Command not equal: %v\nGot: %v\nWant: %v", + c.cmd, new_cmd, c.truth) + } + } + // Arguments parsing tests + for _, c := range []struct { + cmd []string + truth []string + }{ + {[]string{"$2", "$3"}, []string{"my_arg1", "my_arg2"}}, + {[]string{"$2", "$3", "$4"}, []string{"my_arg1", "my_arg2", "my_arg3"}}, + {[]string{"$2", "dont_change_arg"}, []string{"my_arg1", "dont_change_arg"}}, + {[]string{"keep_arg", "$3"}, []string{"keep_arg", "my_arg2"}}, + } { + new_cmd := convertArgs(matches, c.cmd) + if !reflect.DeepEqual(c.truth, new_cmd) { + t.Errorf("Command not equal: %v\nGot: %v\nWant: %v", + c.cmd, new_cmd, c.truth) + } + } +} + +func TestReadConfigs(t *testing.T) { + optcmd := readConfigs("output_runner_test.json") + + for idx, c := range []struct { + regex string + command string + args []string + }{ + {"^(buildozer) '(.*)'\\s+(.*)$", "$1", []string{"$2", "$3"}}, + {"WARNING", "warn", []string{"keep_calm", "dont_panic"}}, + {"DANGER", "danger", []string{"be_careful", "why_so_serious"}}, + } { + if !reflect.DeepEqual(c.regex, optcmd[idx].Regex) { + t.Errorf("Regex not equal: %v\nGot: %v\nWant: %v", + optcmd[idx], optcmd[idx].Regex, c.regex) + } + if !reflect.DeepEqual(c.command, optcmd[idx].Command) { + t.Errorf("Command not equal: %v\nGot: %v\nWant: %v", + optcmd[idx], optcmd[idx].Command, c.command) + } + if !reflect.DeepEqual(c.args, optcmd[idx].Args) { + t.Errorf("Args not equal: %v\nGot: %v\nWant: %v", + optcmd[idx], optcmd[idx].Args, c.args) + } + } +} + +func TestMatchRegex(t *testing.T) { + buf := bytes.Buffer{} + buf.WriteString("buildozer 'add deps test_dep1' //target1:target1\n") + buf.WriteString("buildozer 'add deps test_dep2' //target2:target2\n") + buf.WriteString("buildifier 'cmd_nvm' //target_nvm:target_nvm\n") + buf.WriteString("not_a_match 'nvm' //target_nvm:target_nvm\n") + + optcmd := []Optcmd{ + {Regex: "^(buildozer) '(.*)'\\s+(.*)$", Command: "$1", Args: []string{"$2", "$3"}}, + {Regex: "^(buildifier) '(.*)'\\s+(.*)$", Command: "test_cmd", Args: []string{"test_arg1", "test_arg2"}}, + } + + _, commands, args := matchRegex(optcmd, &buf) + + for idx, c := range []struct { + cls string + cs string + as []string + }{ + {"buildozer 'add deps test_dep1' //target1:target1", "buildozer", []string{"add deps test_dep1", "//target1:target1"}}, + {"buildozer 'add deps test_dep2' //target2:target2", "buildozer", []string{"add deps test_dep2", "//target2:target2"}}, + {"buildifier 'cmd_nvm' //target_nvm:target_nvm", "test_cmd", []string{"test_arg1", "test_arg2"}}, + } { + if !reflect.DeepEqual(c.cs, commands[idx]) { + t.Errorf("Commands not equal: %v\nGot: %v\nWant: %v", + c.cls, commands[idx], c.cs) + } + if !reflect.DeepEqual(c.as, args[idx]) { + t.Errorf("Arguments not equal: %v\nGot: %v\nWant: %v", + c.cls, args[idx], c.as) + } + } +} diff --git a/ibazel/output_runner/output_runner_test.json b/ibazel/output_runner/output_runner_test.json new file mode 100644 index 00000000..4045cedd --- /dev/null +++ b/ibazel/output_runner/output_runner_test.json @@ -0,0 +1,17 @@ +[ + { + "regex": "^(buildozer) '(.*)'\\s+(.*)$", + "command": "$1", + "args": ["$2", "$3"] + }, + { + "regex": "WARNING", + "command": "warn", + "args": ["keep_calm", "dont_panic"] + }, + { + "regex": "DANGER", + "command": "danger", + "args": ["be_careful", "why_so_serious"] + } +] diff --git a/ibazel/profiler/profiler.go b/ibazel/profiler/profiler.go index 0021ad81..99f3fc5c 100644 --- a/ibazel/profiler/profiler.go +++ b/ibazel/profiler/profiler.go @@ -15,6 +15,7 @@ package profiler import ( + "bytes" "encoding/json" "errors" "flag" @@ -142,7 +143,7 @@ func (i *Profiler) BeforeCommand(targets []string, command string) { } } -func (i *Profiler) AfterCommand(targets []string, command string, success bool) { +func (i *Profiler) AfterCommand(targets []string, command string, success bool, output *bytes.Buffer) { if i.file == nil { return } diff --git a/ibazel/workspace_finder/BUILD b/ibazel/workspace_finder/BUILD new file mode 100644 index 00000000..38544430 --- /dev/null +++ b/ibazel/workspace_finder/BUILD @@ -0,0 +1,28 @@ +# 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 = [ + "workspace_finder.go", + ], + importpath = "github.com/bazelbuild/bazel-watcher/ibazel/workspace_finder", + visibility = ["//ibazel:__subpackages__"], + deps = [ + "//third_party/bazel/master/src/main/protobuf:go_default_library", + "//bazel:go_default_library", + ], +) diff --git a/ibazel/workspace_finder.go b/ibazel/workspace_finder/workspace_finder.go similarity index 98% rename from ibazel/workspace_finder.go rename to ibazel/workspace_finder/workspace_finder.go index 8caa9171..d470d6aa 100644 --- a/ibazel/workspace_finder.go +++ b/ibazel/workspace_finder/workspace_finder.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package workspace_finder import ( "errors"