@@ -3,123 +3,150 @@ package run
33import (
44 "context"
55 "fmt"
6+ "io"
67 "os/exec"
7- "strings"
88
99 "github.com/rjeczalik/notify"
10-
1110 "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets"
11+ "github.com/sourcegraph/sourcegraph/dev/sg/internal/std"
12+ "github.com/sourcegraph/sourcegraph/lib/errors"
13+ "github.com/sourcegraph/sourcegraph/lib/output"
14+ "github.com/sourcegraph/sourcegraph/lib/process"
1215)
1316
1417// A BazelCommand is a command definition for sg run/start that uses
1518// bazel under the hood. It will handle restarting itself autonomously,
1619// as long as iBazel is running and watch that specific target.
1720type BazelCommand struct {
18- Name string
19- Description string `yaml:"description"`
20- Target string `yaml:"target"`
21- Args string `yaml:"args"`
22- PreCmd string `yaml:"precmd"`
23- Env map [string ]string `yaml:"env"`
24- IgnoreStdout bool `yaml:"ignoreStdout"`
25- IgnoreStderr bool `yaml:"ignoreStderr"`
26- ContinueWatchOnExit bool `yaml:"continueWatchOnExit"`
27- // Preamble is a short and visible message, displayed when the command is launched.
28- Preamble string `yaml:"preamble"`
21+ Name string
22+ Description string `yaml:"description"`
23+ Target string `yaml:"target"`
24+ Args string `yaml:"args"`
25+ PreCmd string `yaml:"precmd"`
26+ Env map [string ]string `yaml:"env"`
27+ IgnoreStdout bool `yaml:"ignoreStdout"`
28+ IgnoreStderr bool `yaml:"ignoreStderr"`
2929 ExternalSecrets map [string ]secrets.ExternalSecret `yaml:"external_secrets"`
30-
31- // RunTarget specifies a target that should be run via `bazel run $RunTarget` instead of directly executing the binary.
32- RunTarget string `yaml:"runTarget"`
3330}
3431
35- func (bc BazelCommand ) GetName () string {
36- return bc .Name
32+ func (bc * BazelCommand ) BinLocation () ( string , error ) {
33+ return binLocation ( bc .Target )
3734}
3835
39- func (bc BazelCommand ) GetContinueWatchOnExit () bool {
40- return bc .ContinueWatchOnExit
41- }
36+ func (bc * BazelCommand ) watch (ctx context.Context ) (<- chan struct {}, error ) {
37+ // Grab the location of the binary in bazel-out.
38+ binLocation , err := bc .BinLocation ()
39+ if err != nil {
40+ return nil , err
41+ }
4242
43- func (bc BazelCommand ) GetEnv () map [string ]string {
44- return bc .Env
45- }
43+ // Set up the watcher.
44+ restart := make (chan struct {})
45+ events := make (chan notify.EventInfo , 1 )
46+ if err := notify .Watch (binLocation , events , notify .All ); err != nil {
47+ return nil , err
48+ }
4649
47- func (bc BazelCommand ) GetIgnoreStdout () bool {
48- return bc .IgnoreStdout
49- }
50+ // Start watching for a freshly compiled version of the binary.
51+ go func () {
52+ defer close (events )
53+ defer notify .Stop (events )
54+
55+ for {
56+ select {
57+ case <- ctx .Done ():
58+ return
59+ case e := <- events :
60+ if e .Event () != notify .Remove {
61+ restart <- struct {}{}
62+ }
63+ }
5064
51- func (bc BazelCommand ) GetIgnoreStderr () bool {
52- return bc .IgnoreStderr
53- }
65+ }
66+ }()
5467
55- func (bc BazelCommand ) GetPreamble () string {
56- return bc .Preamble
68+ return restart , nil
5769}
5870
59- func (bc BazelCommand ) GetBinaryLocation () (string , error ) {
60- baseOutput , err := outputPath ()
71+ func (bc * BazelCommand ) Start (ctx context.Context , dir string , parentEnv map [string ]string ) error {
72+ std .Out .WriteLine (output .Styledf (output .StylePending , "Running %s..." , bc .Name ))
73+
74+ // Run the binary for the first time.
75+ cancel , err := bc .start (ctx , dir , parentEnv )
6176 if err != nil {
62- return " " , err
77+ return errors . Wrapf ( err , "failed to start Bazel command %q " , bc . Name )
6378 }
64- // Trim "bazel-out" because the next bazel query will include it.
65- outputPath := strings .TrimSuffix (strings .TrimSpace (string (baseOutput )), "bazel-out" )
6679
67- // Get the binary from the specific target.
68- cmd := exec .Command ("bazel" , "cquery" , bc .Target , "--output=files" )
69- baseOutput , err = cmd .Output ()
80+ // Restart when the binary change.
81+ wantRestart , err := bc .watch (ctx )
7082 if err != nil {
71- return "" , err
83+ return err
7284 }
73- binPath := strings .TrimSpace (string (baseOutput ))
7485
75- return fmt .Sprintf ("%s%s" , outputPath , binPath ), nil
86+ // Wait forever until we're asked to stop or that restarting returns an error.
87+ for {
88+ select {
89+ case <- ctx .Done ():
90+ return ctx .Err ()
91+ case <- wantRestart :
92+ std .Out .WriteLine (output .Styledf (output .StylePending , "Restarting %s..." , bc .Name ))
93+ cancel ()
94+ cancel , err = bc .start (ctx , dir , parentEnv )
95+ if err != nil {
96+ return err
97+ }
98+ }
99+ }
76100}
77101
78- func (bc BazelCommand ) GetExternalSecrets () map [string ]secrets.ExternalSecret {
79- return bc .ExternalSecrets
80- }
102+ func (bc * BazelCommand ) start (ctx context.Context , dir string , parentEnv map [string ]string ) (func (), error ) {
103+ binLocation , err := bc .BinLocation ()
104+ if err != nil {
105+ return nil , err
106+ }
81107
82- func (bc BazelCommand ) watchPaths () ([]string , error ) {
83- // If no target is defined, there is nothing to be built and watched
84- if bc .Target == "" {
85- return nil , nil
108+ sc := & startedCmd {
109+ stdoutBuf : & prefixSuffixSaver {N : 32 << 10 },
110+ stderrBuf : & prefixSuffixSaver {N : 32 << 10 },
86111 }
87- // Grab the location of the binary in bazel-out.
88- binLocation , err := bc .GetBinaryLocation ()
112+
113+ commandCtx , cancel := context .WithCancel (ctx )
114+ sc .cancel = cancel
115+ sc .Cmd = exec .CommandContext (commandCtx , "bash" , "-c" , fmt .Sprintf ("%s\n %s" , bc .PreCmd , binLocation ))
116+ sc .Cmd .Dir = dir
117+
118+ secretsEnv , err := getSecrets (ctx , bc .Name , bc .ExternalSecrets )
89119 if err != nil {
90- return nil , err
120+ std .Out .WriteLine (output .Styledf (output .StyleWarning , "[%s] %s %s" ,
121+ bc .Name , output .EmojiFailure , err .Error ()))
91122 }
92- return []string {binLocation }, nil
93123
94- }
124+ sc . Cmd . Env = makeEnv ( parentEnv , secretsEnv , bc . Env )
95125
96- func (bc BazelCommand ) StartWatch (ctx context.Context ) (<- chan struct {}, error ) {
97- if watchPaths , err := bc .watchPaths (); err != nil {
98- return nil , err
126+ var stdoutWriter , stderrWriter io.Writer
127+ logger := newCmdLogger (commandCtx , bc .Name , std .Out .Output )
128+ if bc .IgnoreStdout {
129+ std .Out .WriteLine (output .Styledf (output .StyleSuggestion , "Ignoring stdout of %s" , bc .Name ))
130+ stdoutWriter = sc .stdoutBuf
99131 } else {
100- // skip remove events as we don't care about files being removed, we only
101- // want to know when the binary has been rebuilt
102- return WatchPaths (ctx , watchPaths , notify .Remove )
132+ stdoutWriter = io .MultiWriter (logger , sc .stdoutBuf )
103133 }
104- }
105-
106- func (bc BazelCommand ) GetExecCmd (ctx context.Context ) (* exec.Cmd , error ) {
107- var cmd string
108- var err error
109- if bc .RunTarget != "" {
110- cmd = "bazel run " + bc .RunTarget
134+ if bc .IgnoreStderr {
135+ std .Out .WriteLine (output .Styledf (output .StyleSuggestion , "Ignoring stderr of %s" , bc .Name ))
136+ stderrWriter = sc .stderrBuf
111137 } else {
112- if cmd , err = bc .GetBinaryLocation (); err != nil {
113- return nil , err
114- }
138+ stderrWriter = io .MultiWriter (logger , sc .stderrBuf )
115139 }
116140
117- return exec .CommandContext (ctx , "bash" , "-c" , fmt .Sprintf ("%s\n %s" , bc .PreCmd , cmd )), nil
118- }
141+ eg , err := process .PipeOutputUnbuffered (ctx , sc .Cmd , stdoutWriter , stderrWriter )
142+ if err != nil {
143+ return nil , err
144+ }
145+ sc .outEg = eg
146+
147+ if err := sc .Start (); err != nil {
148+ return nil , err
149+ }
119150
120- func outputPath () ([]byte , error ) {
121- // Get the output directory from Bazel, which varies depending on which OS
122- // we're running against.
123- cmd := exec .Command ("bazel" , "info" , "output_path" )
124- return cmd .Output ()
151+ return cancel , nil
125152}
0 commit comments