From fcc49fc50baa6fd9706b79cb192bcdd554ed4edd Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 18 Jan 2024 20:11:16 -0800 Subject: [PATCH 01/17] WIP draft --- .bazel_fix_commands.json | 18 +- cmd/frontend/main.go | 2 + cmd/worker/BUILD.bazel | 2 +- dev/bazel_configure_accept_changes.sh | 16 + dev/sg/internal/run/BUILD.bazel | 9 +- dev/sg/internal/run/bazel_build.go | 64 -- dev/sg/internal/run/bazel_command.go | 170 ++--- dev/sg/internal/run/command.go | 263 ++++++-- dev/sg/internal/run/config_command.go | 80 +++ dev/sg/internal/run/ibazel.go | 191 +++++- dev/sg/internal/run/installer.go | 252 ++++++++ dev/sg/internal/run/run.go | 606 ++++-------------- dev/sg/internal/run/run_bazel.go | 73 --- dev/sg/sg_run.go | 32 +- dev/sg/sg_start.go | 116 ++-- dev/sg/sg_tests.go | 29 +- go.mod | 2 + go.sum | 2 + .../uploads/internal/store/processing.go | 2 + lib/errors/filter.go | 1 + sg.config.yaml | 20 +- 21 files changed, 1059 insertions(+), 891 deletions(-) create mode 100755 dev/bazel_configure_accept_changes.sh delete mode 100644 dev/sg/internal/run/bazel_build.go create mode 100644 dev/sg/internal/run/config_command.go create mode 100644 dev/sg/internal/run/installer.go delete mode 100644 dev/sg/internal/run/run_bazel.go diff --git a/.bazel_fix_commands.json b/.bazel_fix_commands.json index fe51488c7066..b128df67c1f2 100644 --- a/.bazel_fix_commands.json +++ b/.bazel_fix_commands.json @@ -1 +1,17 @@ -[] +[ + { + "regex": "^Check that imports in Go sources match importpath attributes in deps.$", + "command": "./dev/bazel_configure_accept_changes.sh", + "args": [] + }, + { + "regex": "missing input file", + "command": "./dev/bazel_configure_accept_changes.sh", + "args": [] + }, + { + "regex": ": undefined:", + "command": "./dev/bazel_configure_accept_changes.sh", + "args": [] + } +] diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index a744d44d7cd1..5f83c6222382 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -16,6 +16,8 @@ import ( func main() { sanitycheck.Pass() + println("SUP") + // os.Exit(10) if os.Getenv("WEB_BUILDER_DEV_SERVER") == "1" { assets.UseDevAssetsProvider() } diff --git a/cmd/worker/BUILD.bazel b/cmd/worker/BUILD.bazel index ed73e3e183a2..11a7e2ceffb1 100644 --- a/cmd/worker/BUILD.bazel +++ b/cmd/worker/BUILD.bazel @@ -1,5 +1,5 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") load("@container_structure_test//:defs.bzl", "container_structure_test") +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") load("@rules_oci//oci:defs.bzl", "oci_image", "oci_push", "oci_tarball") load("@rules_pkg//:pkg.bzl", "pkg_tar") load("//dev:oci_defs.bzl", "image_repository") diff --git a/dev/bazel_configure_accept_changes.sh b/dev/bazel_configure_accept_changes.sh new file mode 100755 index 000000000000..b6eb3fbf4cd3 --- /dev/null +++ b/dev/bazel_configure_accept_changes.sh @@ -0,0 +1,16 @@ +#! /bin/bash + +# Run bazel configure and if the error code is 110, exit with error code 0 +bazel configure +exit_code=$? + +if [ $exit_code -eq 0 ]; then + echo "No configuration changes made" + exit 0 +elif [ $exit_code -eq 110 ]; then + echo "Bazel configuration completed" + exit 0 +else + echo "Unknown error" + exit $exit_code +fi diff --git a/dev/sg/internal/run/BUILD.bazel b/dev/sg/internal/run/BUILD.bazel index 08aa42ff93d4..60940e789220 100644 --- a/dev/sg/internal/run/BUILD.bazel +++ b/dev/sg/internal/run/BUILD.bazel @@ -1,19 +1,19 @@ -load("//dev:go_defs.bzl", "go_test") load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//dev:go_defs.bzl", "go_test") go_library( name = "run", srcs = [ - "bazel_build.go", "bazel_command.go", "command.go", + "config_command.go", "helpers.go", "ibazel.go", + "installer.go", "logger.go", "pid.go", "prefix_suffix_saver.go", "run.go", - "run_bazel.go", ], importpath = "github.com/sourcegraph/sourcegraph/dev/sg/internal/run", visibility = ["//dev/sg:__subpackages__"], @@ -27,10 +27,9 @@ go_library( "//lib/errors", "//lib/output", "//lib/process", - "@com_github_grafana_regexp//:regexp", + "@com_github_nxadm_tail//:tail", "@com_github_rjeczalik_notify//:notify", "@com_github_sourcegraph_conc//pool", - "@org_golang_x_sync//semaphore", ], ) diff --git a/dev/sg/internal/run/bazel_build.go b/dev/sg/internal/run/bazel_build.go deleted file mode 100644 index a258d8f59a00..000000000000 --- a/dev/sg/internal/run/bazel_build.go +++ /dev/null @@ -1,64 +0,0 @@ -package run - -import ( - "context" - "fmt" - "io" - "os/exec" - - "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" - "github.com/sourcegraph/sourcegraph/dev/sg/root" - "github.com/sourcegraph/sourcegraph/lib/output" - "github.com/sourcegraph/sourcegraph/lib/process" -) - -// BazelBuild peforms a bazel build command with all the given targets and blocks until an -// error is returned or the build is completed. -func BazelBuild(ctx context.Context, cmds ...BazelCommand) error { - if len(cmds) == 0 { - // no Bazel commands so we return - return nil - } - std.Out.WriteLine(output.Styled(output.StylePending, fmt.Sprintf("Detected %d bazel targets, running bazel build before anything else", len(cmds)))) - - repoRoot, err := root.RepositoryRoot() - if err != nil { - return err - } - - targets := make([]string, 0, len(cmds)) - for _, cmd := range cmds { - targets = append(targets, cmd.Target) - } - - var cancel func() - ctx, cancel = context.WithCancel(ctx) - - args := append([]string{"build"}, targets...) - cmd := exec.CommandContext(ctx, "bazel", args...) - - sc := &startedCmd{ - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, - } - - sc.cancel = cancel - sc.Cmd = cmd - sc.Cmd.Dir = repoRoot - - var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(ctx, "bazel", std.Out.Output) - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf) - stderrWriter = io.MultiWriter(logger, sc.stderrBuf) - eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) - if err != nil { - return err - } - sc.outEg = eg - - // Bazel out directory should exist here before returning - if err := sc.Start(); err != nil { - return err - } - return sc.Wait() -} diff --git a/dev/sg/internal/run/bazel_command.go b/dev/sg/internal/run/bazel_command.go index c3d07b744f9f..924fc6ad1c17 100644 --- a/dev/sg/internal/run/bazel_command.go +++ b/dev/sg/internal/run/bazel_command.go @@ -3,150 +3,108 @@ package run import ( "context" "fmt" - "io" "os/exec" + "strings" - "github.com/rjeczalik/notify" "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" - "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" - "github.com/sourcegraph/sourcegraph/lib/errors" - "github.com/sourcegraph/sourcegraph/lib/output" - "github.com/sourcegraph/sourcegraph/lib/process" ) // A BazelCommand is a command definition for sg run/start that uses // bazel under the hood. It will handle restarting itself autonomously, // as long as iBazel is running and watch that specific target. type BazelCommand struct { - Name string - Description string `yaml:"description"` - Target string `yaml:"target"` - Args string `yaml:"args"` - PreCmd string `yaml:"precmd"` - Env map[string]string `yaml:"env"` - IgnoreStdout bool `yaml:"ignoreStdout"` - IgnoreStderr bool `yaml:"ignoreStderr"` + Name string + Description string `yaml:"description"` + Target string `yaml:"target"` + Args string `yaml:"args"` + PreCmd string `yaml:"precmd"` + Env map[string]string `yaml:"env"` + IgnoreStdout bool `yaml:"ignoreStdout"` + IgnoreStderr bool `yaml:"ignoreStderr"` + ContinueWatchOnExit bool `yaml:"continueWatchOnExit"` + // Preamble is a short and visible message, displayed when the command is launched. + Preamble string `yaml:"preamble"` ExternalSecrets map[string]secrets.ExternalSecret `yaml:"external_secrets"` } -func (bc *BazelCommand) BinLocation() (string, error) { - return binLocation(bc.Target) +func (bc BazelCommand) GetName() string { + return bc.Name } -func (bc *BazelCommand) watch(ctx context.Context) (<-chan struct{}, error) { - // Grab the location of the binary in bazel-out. - binLocation, err := bc.BinLocation() - if err != nil { - return nil, err - } +func (bc BazelCommand) GetContinueWatchOnExit() bool { + return bc.ContinueWatchOnExit +} - // Set up the watcher. - restart := make(chan struct{}) - events := make(chan notify.EventInfo, 1) - if err := notify.Watch(binLocation, events, notify.All); err != nil { - return nil, err - } +func (bc BazelCommand) GetEnv() map[string]string { + return bc.Env +} - // Start watching for a freshly compiled version of the binary. - go func() { - defer close(events) - defer notify.Stop(events) - - for { - select { - case <-ctx.Done(): - return - case e := <-events: - if e.Event() != notify.Remove { - restart <- struct{}{} - } - } - - } - }() - - return restart, nil +func (bc BazelCommand) GetIgnoreStdout() bool { + return bc.IgnoreStdout } -func (bc *BazelCommand) Start(ctx context.Context, dir string, parentEnv map[string]string) error { - std.Out.WriteLine(output.Styledf(output.StylePending, "Running %s...", bc.Name)) +func (bc BazelCommand) GetIgnoreStderr() bool { + return bc.IgnoreStderr +} - // Run the binary for the first time. - cancel, err := bc.start(ctx, dir, parentEnv) - if err != nil { - return errors.Wrapf(err, "failed to start Bazel command %q", bc.Name) - } +func (bc BazelCommand) GetPreamble() string { + return bc.Preamble +} - // Restart when the binary change. - wantRestart, err := bc.watch(ctx) +func (bc BazelCommand) GetBinaryLocation() (string, error) { + baseOutput, err := outputPath() if err != nil { - return err + return "", err } + // Trim "bazel-out" because the next bazel query will include it. + outputPath := strings.TrimSuffix(strings.TrimSpace(string(baseOutput)), "bazel-out") - // Wait forever until we're asked to stop or that restarting returns an error. - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-wantRestart: - std.Out.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", bc.Name)) - cancel() - cancel, err = bc.start(ctx, dir, parentEnv) - if err != nil { - return err - } - } - } -} - -func (bc *BazelCommand) start(ctx context.Context, dir string, parentEnv map[string]string) (func(), error) { - binLocation, err := bc.BinLocation() + // Get the binary from the specific target. + cmd := exec.Command("bazel", "cquery", bc.Target, "--output=files") + baseOutput, err = cmd.Output() if err != nil { - return nil, err + return "", err } + binPath := strings.TrimSpace(string(baseOutput)) - sc := &startedCmd{ - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, - } + return fmt.Sprintf("%s%s", outputPath, binPath), nil +} - commandCtx, cancel := context.WithCancel(ctx) - sc.cancel = cancel - sc.Cmd = exec.CommandContext(commandCtx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, binLocation)) - sc.Cmd.Dir = dir +func (bc BazelCommand) GetExternalSecrets() map[string]secrets.ExternalSecret { + return bc.ExternalSecrets +} - secretsEnv, err := getSecrets(ctx, bc.Name, bc.ExternalSecrets) +func (bc BazelCommand) watchPaths() ([]string, error) { + // Grab the location of the binary in bazel-out. + binLocation, err := bc.GetBinaryLocation() if err != nil { - std.Out.WriteLine(output.Styledf(output.StyleWarning, "[%s] %s %s", - bc.Name, output.EmojiFailure, err.Error())) + return nil, err } + return []string{binLocation}, nil - sc.Cmd.Env = makeEnv(parentEnv, secretsEnv, bc.Env) +} - var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(commandCtx, bc.Name, std.Out.Output) - if bc.IgnoreStdout { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", bc.Name)) - stdoutWriter = sc.stdoutBuf - } else { - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf) - } - if bc.IgnoreStderr { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", bc.Name)) - stderrWriter = sc.stderrBuf +func (bc BazelCommand) StartWatch(ctx context.Context) (<-chan struct{}, error) { + if watchPaths, err := bc.watchPaths(); err != nil { + return nil, err } else { - stderrWriter = io.MultiWriter(logger, sc.stderrBuf) + return WatchPaths(ctx, watchPaths) } +} - eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) +func (bc BazelCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { + binLocation, err := bc.GetBinaryLocation() if err != nil { return nil, err } - sc.outEg = eg + println("Binary location: " + binLocation + "\n") - if err := sc.Start(); err != nil { - return nil, err - } + return exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, binLocation)), nil +} - return cancel, nil +func outputPath() ([]byte, error) { + // Get the output directory from Bazel, which varies depending on which OS + // we're running against. + cmd := exec.Command("bazel", "info", "output_path") + return cmd.Output() } diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 6926631aab99..060711d3b322 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -5,12 +5,15 @@ import ( "fmt" "io" "net" + "os" "os/exec" + "path/filepath" "github.com/sourcegraph/conc/pool" "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/dev/sg/root" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" "github.com/sourcegraph/sourcegraph/lib/process" @@ -38,6 +41,111 @@ type Command struct { // field in `Merge` (below). } +func (cmd Command) GetName() string { + return cmd.Name +} + +func (cmd Command) GetContinueWatchOnExit() bool { + return cmd.ContinueWatchOnExit +} + +func (cmd Command) GetBinaryLocation() (string, error) { + if cmd.CheckBinary != "" { + repoRoot, err := root.RepositoryRoot() + if err != nil { + return "", err + } + return filepath.Join(repoRoot, cmd.CheckBinary), nil + } + return "", noBinaryError{name: cmd.Name} +} + +func (cmd Command) GetExternalSecrets() map[string]secrets.ExternalSecret { + return cmd.ExternalSecrets +} + +func (cmd Command) GetIgnoreStdout() bool { + return cmd.IgnoreStdout +} + +func (cmd Command) GetIgnoreStderr() bool { + return cmd.IgnoreStderr +} + +func (cmd Command) GetPreamble() string { + return cmd.Preamble +} + +func (cmd Command) GetEnv() map[string]string { + return cmd.Env +} + +func (cmd Command) GetExec(ctx context.Context) (*exec.Cmd, error) { + return exec.CommandContext(ctx, "bash", "-c", cmd.Cmd), nil +} + +func (cmd Command) RunInstall(ctx context.Context, parentEnv map[string]string) error { + if cmd.requiresInstall() { + if cmd.hasBashInstaller() { + return cmd.bashInstall(ctx, parentEnv) + } else { + return cmd.functionInstall(ctx, parentEnv) + } + } + + return nil +} + +func (cmd Command) requiresInstall() bool { + return cmd.Install != "" || cmd.InstallFunc != "" +} + +func (cmd Command) hasBashInstaller() bool { + return cmd.Install != "" || cmd.InstallFunc == "" +} + +func (cmd Command) bashInstall(ctx context.Context, parentEnv map[string]string) error { + output, err := BashInRoot(ctx, cmd.Install, makeEnv(parentEnv, cmd.Env)) + if err != nil { + return installErr{cmdName: cmd.Name, output: output, originalErr: err} + } + return nil +} + +func (cmd Command) functionInstall(ctx context.Context, parentEnv map[string]string) error { + fn, ok := installFuncs[cmd.InstallFunc] + if !ok { + return installErr{cmdName: cmd.Name, originalErr: errors.Newf("no install func with name %q found", cmd.InstallFunc)} + } + if err := fn(ctx, makeEnvMap(parentEnv, cmd.Env)); err != nil { + return installErr{cmdName: cmd.Name, originalErr: err} + } + + return nil +} + +func (cmd Command) watchPaths() ([]string, error) { + root, err := root.RepositoryRoot() + if err != nil { + return nil, err + } + + fullPaths := make([]string, len(cmd.Watch)) + for i, path := range cmd.Watch { + fullPaths[i] = filepath.Join(root, path) + } + + return fullPaths, nil +} + +func (cmd Command) StartWatch(ctx context.Context) (<-chan struct{}, error) { + if watchPaths, err := cmd.watchPaths(); err != nil { + return nil, err + } else { + return WatchPaths(ctx, watchPaths) + } +} + func (c Command) Merge(other Command) Command { merged := c @@ -113,16 +221,44 @@ type startedCmd struct { stdoutBuf *prefixSuffixSaver stderrBuf *prefixSuffixSaver - outEg *pool.ErrorPool + outEg *pool.ErrorPool + result chan error + + opts commandOptions +} + +func (sc *startedCmd) ErrorChannel() <-chan error { + if sc.result == nil { + sc.result = make(chan error) + go func() { + defer close(sc.result) + sc.result <- sc.Wait() + }() + } + return sc.result } func (sc *startedCmd) Wait() error { + err := sc.wait() + var e *exec.ExitError + if errors.As(err, &e) { + err = runErr{ + cmdName: sc.opts.name, + exitCode: e.ExitCode(), + stderr: sc.CapturedStderr(), + stdout: sc.CapturedStdout(), + } + } + + return err +} + +func (sc *startedCmd) wait() error { if err := sc.outEg.Wait(); err != nil { return err } return sc.Cmd.Wait() } - func (sc *startedCmd) CapturedStdout() string { if sc.stdoutBuf == nil { return "" @@ -170,76 +306,103 @@ func OpenUnixSocket() error { return err } -func startCmd(ctx context.Context, dir string, cmd Command, parentEnv map[string]string) (*startedCmd, error) { +func startConfigCmd(ctx context.Context, cmd ConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { + exec, err := cmd.GetExec(ctx) + if err != nil { + return nil, err + } + + secretsEnv, err := getSecrets(ctx, cmd.GetName(), cmd.GetExternalSecrets()) + if err != nil { + std.Out.WriteLine(output.Styledf(output.StyleWarning, "[%s] %s %s", + cmd.GetName(), output.EmojiFailure, err.Error())) + } + + opts := commandOptions{ + name: cmd.GetName(), + exec: exec, + env: makeEnv(parentEnv, secretsEnv, cmd.GetEnv()), + dir: dir, + ignoreStdOut: cmd.GetIgnoreStdout(), + ignoreStdErr: cmd.GetIgnoreStderr(), + } + + if cmd.GetPreamble() != "" { + std.Out.WriteLine(output.Styledf(output.StyleOrange, "[%s] %s %s", cmd.GetName(), output.EmojiInfo, cmd.GetPreamble())) + } + + return startCmd(ctx, opts) +} + +type commandOptions struct { + name string + exec *exec.Cmd + dir string + env []string + ignoreStdOut bool + ignoreStdErr bool + // when enabled, stdout/stderr will not be streamed to the loggers + // after the process is begun, only captured for later retrieval + bufferOutput bool +} + +func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { sc := &startedCmd{ + opts: opts, stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, stderrBuf: &prefixSuffixSaver{N: 32 << 10}, } - commandCtx, cancel := context.WithCancel(ctx) - sc.cancel = cancel + ctx, cancel := context.WithCancel(ctx) + sc.cancel = func() { + sc.Cmd.Process.Signal(os.Interrupt) + cancel() + } - sc.Cmd = exec.CommandContext(commandCtx, "bash", "-c", cmd.Cmd) - sc.Cmd.Dir = dir + sc.Cmd = opts.exec + sc.Cmd.Dir = opts.dir + sc.Cmd.Env = opts.env - secretsEnv, err := getSecrets(ctx, cmd.Name, cmd.ExternalSecrets) - if err != nil { - std.Out.WriteLine(output.Styledf(output.StyleWarning, "[%s] %s %s", - cmd.Name, output.EmojiFailure, err.Error())) + if !opts.bufferOutput { + if err := sc.connectOutput(ctx); err != nil { + return nil, err + } } - sc.Cmd.Env = makeEnv(parentEnv, secretsEnv, cmd.Env) + return sc, sc.Start() +} + +func (sc *startedCmd) connectOutput(ctx context.Context) error { var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(commandCtx, cmd.Name, std.Out.Output) + logger := newCmdLogger(ctx, sc.opts.name, std.Out.Output) - // TODO(JH) sgtail experiment going on, this is a bit ugly, that will do it - // for the demo day. + var sgConnLog io.Writer = io.Discard if sgConn != nil { sink := func(data string) { - sgConn.Write([]byte(fmt.Sprintf("%s: %s\n", cmd.Name, data))) + sgConn.Write([]byte(fmt.Sprintf("%s: %s\n", sc.opts.name, data))) } - sgConnLog := process.NewLogger(ctx, sink) + sgConnLog = process.NewLogger(ctx, sink) + } - if cmd.IgnoreStdout { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", cmd.Name)) - stdoutWriter = sc.stdoutBuf - } else { - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf, sgConnLog) - } - if cmd.IgnoreStderr { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", cmd.Name)) - stderrWriter = sc.stderrBuf - } else { - stderrWriter = io.MultiWriter(logger, sc.stderrBuf, sgConnLog) - } + if sc.opts.ignoreStdOut { + std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", sc.opts.name)) + stdoutWriter = sc.stdoutBuf } else { - if cmd.IgnoreStdout { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", cmd.Name)) - stdoutWriter = sc.stdoutBuf - } else { - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf) - } - if cmd.IgnoreStderr { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", cmd.Name)) - stderrWriter = sc.stderrBuf - } else { - stderrWriter = io.MultiWriter(logger, sc.stderrBuf) - } + stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf, sgConnLog) } - - if cmd.Preamble != "" { - std.Out.WriteLine(output.Styledf(output.StyleOrange, "[%s] %s %s", cmd.Name, output.EmojiInfo, cmd.Preamble)) + if sc.opts.ignoreStdErr { + std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", sc.opts.name)) + stderrWriter = sc.stderrBuf + } else { + stderrWriter = io.MultiWriter(logger, sc.stderrBuf, sgConnLog) } + eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) if err != nil { - return nil, err + return err } sc.outEg = eg - if err := sc.Start(); err != nil { - return sc, err - } - - return sc, nil + return nil } diff --git a/dev/sg/internal/run/config_command.go b/dev/sg/internal/run/config_command.go new file mode 100644 index 000000000000..be9a2cc8660d --- /dev/null +++ b/dev/sg/internal/run/config_command.go @@ -0,0 +1,80 @@ +package run + +import ( + "context" + "fmt" + "os/exec" + + "github.com/rjeczalik/notify" + + "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" +) + +type ConfigCommand interface { + // Getters for common fields + GetName() string + GetContinueWatchOnExit() bool + GetIgnoreStdout() bool + GetIgnoreStderr() bool + GetPreamble() string + GetEnv() map[string]string + GetBinaryLocation() (string, error) + GetExternalSecrets() map[string]secrets.ExternalSecret + GetExec(context.Context) (*exec.Cmd, error) + + // Start a file watcher on the relevant filesystem sub-tree for this command + StartWatch(context.Context) (<-chan struct{}, error) +} + +func WatchPaths(ctx context.Context, paths []string) (<-chan struct{}, error) { + // Set up the watchers. + restart := make(chan struct{}) + events := make(chan notify.EventInfo, 1) + + // Do nothing if no watch paths are configured + if len(paths) == 0 { + return restart, nil + } + + for _, path := range paths { + if err := notify.Watch(path, events, notify.All); err != nil { + return nil, err + } + } + + // Start watching for changes to the source tree + go func() { + defer close(events) + defer notify.Stop(events) + + for { + select { + case <-ctx.Done(): + return + case <-events: + restart <- struct{}{} + } + + } + }() + + return restart, nil +} + +type noBinaryError struct { + name string + err error +} + +func (e noBinaryError) Error() string { + return fmt.Sprintf("no-binary-error: %s has no binary", e.name) +} + +func (e noBinaryError) Unwrap() error { + return e.err +} + +func (e noBinaryError) Wrap(err error) error { + e.err = err + return e +} diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index ee4293eccf93..ae0e14d1fba6 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -2,56 +2,183 @@ package run import ( "context" - "io" + "encoding/json" + "fmt" + "os" "os/exec" + "path" + "slices" + "strings" - "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" - "github.com/sourcegraph/sourcegraph/lib/process" + "github.com/nxadm/tail" + + "github.com/sourcegraph/conc/pool" + + "github.com/sourcegraph/sourcegraph/lib/errors" ) type IBazel struct { - pwd string - targets []string - cancel func() + targets []string + events chan iBazelEvent + eventsDir string + dir string + proc *startedCmd } -// newIBazel returns a runner to interact with ibazel. -func newIBazel(pwd string, targets ...string) *IBazel { - return &IBazel{ - pwd: pwd, - targets: targets, +func (ibazel *IBazel) GetName() string { + return fmt.Sprintf("bazel targets (%s)", strings.Join(ibazel.targets, ", ")) +} + +func (ibazel *IBazel) RunInstall(ctx context.Context, env map[string]string) error { + if len(ibazel.targets) == 0 { + // no Bazel commands so we return + return nil + } + + err := ibazel.Build(ctx) + if err != nil { + return err } + + p := pool.New().WithContext(ctx).WithCancelOnError() + + p.Go(func(ctx context.Context) error { + return ibazel.Watch(ctx) + }) + + // block until initial ibazel build is completed + return ibazel.WaitForInitialBuild(ctx) } -func (ib *IBazel) Start(ctx context.Context, dir string) error { - args := append([]string{"build"}, ib.targets...) - ctx, ib.cancel = context.WithCancel(ctx) - cmd := exec.CommandContext(ctx, "ibazel", args...) +func (ib *IBazel) GetExec(ctx context.Context) *exec.Cmd { + // Writes iBazel events out to a log file. These are much easier to parse + // than trying to understand the output directly + profilePath := "--profile_dev=" + ib.profileEventsFilePath() + // This enables iBazel to try to apply the fixes from .bazel_fix_commands.json automatically + enableAutoFix := "--run_output_interactive=false" + args := append([]string{profilePath, enableAutoFix, "build"}, ib.targets...) + return exec.CommandContext(ctx, "ibazel", args...) +} - sc := &startedCmd{ - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, +// returns a runner to interact with ibazel. +func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { + eventsDir, err := os.MkdirTemp("", "ibazel-events") + if err != nil { + return nil, err + } + eventsFile, err := os.Create(profileEventsFilePath(eventsDir)) + if err != nil { + return nil, err + } + if err = eventsFile.Close(); err != nil { + return nil, err } - sc.cancel = ib.cancel - sc.Cmd = cmd - sc.Cmd.Dir = dir + targets := make([]string, 0, len(cmds)) + for _, cmd := range cmds { + if !slices.Contains(targets, cmd.Target) { + targets = append(targets, cmd.Target) + } + } + + return &IBazel{ + targets: targets, + events: make(chan iBazelEvent), + eventsDir: eventsDir, + dir: dir, + }, nil +} + +func (ib *IBazel) profileEventsFilePath() string { + return profileEventsFilePath(ib.eventsDir) +} - var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(ctx, "iBazel", std.Out.Output) - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf) - stderrWriter = io.MultiWriter(logger, sc.stderrBuf) - eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) +func profileEventsFilePath(eventsDir string) string { + return path.Join(eventsDir, "profile.json") +} + +// Watch opens the provided profile.json and reads it as it is continuously written by iBazel +// Each time it sees a iBazel event log, it parses it and puts it on the events channel +func (ib *IBazel) Watch(ctx context.Context) error { + tail, err := tail.TailFile(ib.profileEventsFilePath(), tail.Config{Follow: true, ReOpen: true}) if err != nil { return err } - sc.outEg = eg - - // Bazel out directory should exist here before returning - return sc.Start() + for line := range tail.Lines { + var event iBazelEvent + if err := json.Unmarshal([]byte(line.Text), &event); err != nil { + return errors.Newf("failed to unmarshal event json: %s", err) + } + ib.events <- event + } + return nil } -func (ib *IBazel) Stop() error { - ib.cancel() +func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { + for event := range ib.events { + if event.Type == buildDone { + return nil + } + if event.Type == buildFailed { + return errors.Newf("initial ibazel build failed") + } + } + return nil } + +func (ib *IBazel) getCommandOptions(ctx context.Context) commandOptions { + return commandOptions{ + name: ib.GetName(), + exec: ib.GetExec(ctx), + dir: ib.dir, + // Don't output iBazel logs until initial build is complete + // as it will break the progress bar + bufferOutput: true, + } +} + +// Build starts an ibazel process to build the targets provided in the constructor +// It runs perpetually, watching for file changes +func (ib *IBazel) Build(ctx context.Context) (err error) { + ib.proc, err = startCmd(ctx, ib.getCommandOptions(ctx)) + return err +} + +func (ib *IBazel) Stop() { + os.RemoveAll(ib.eventsDir) + ib.proc.cancel() +} + +// Schema information at https://github.com/bazelbuild/bazel-watcher?tab=readme-ov-file#profiler-events +type iBazelEvent struct { + // common + Type string `json:"type"` + Iteration string `json:"iteration"` + Time int64 `json:"time"` + Targets []string `json:"targets,omitempty"` + Elapsed int64 `json:"elapsed,omitempty"` + + // start event + IBazelVersion string `json:"iBazelVersion,omitempty"` + BazelVersion string `json:"bazelVersion,omitempty"` + MaxHeapSize string `json:"maxHeapSize,omitempty"` + CommittedHeapSize string `json:"committedHeapSize,omitempty"` + + // change event + Change string `json:"change,omitempty"` + + // build & reload event + Changes []string `json:"changes,omitempty"` + + // browser event + RemoteType string `json:"remoteType,omitempty"` + RemoteTime int64 `json:"remoteTime,omitempty"` + RemoteElapsed int64 `json:"remoteElapsed,omitempty"` + RemoteData string `json:"remoteData,omitempty"` +} + +const ( + buildDone = "BUILD_DONE" + buildFailed = "BUILD_FAILED" +) diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go new file mode 100644 index 000000000000..951c0b2c331b --- /dev/null +++ b/dev/sg/internal/run/installer.go @@ -0,0 +1,252 @@ +package run + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/sourcegraph/sourcegraph/dev/sg/internal/analytics" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/dev/sg/interrupt" + "github.com/sourcegraph/sourcegraph/lib/output" +) + +type Installer interface { + RunInstall(ctx context.Context, env map[string]string) error + GetName() string +} + +type InstallManager struct { + // Constructor commands + out *std.Output + cmds map[string]struct{} + env map[string]string + verbose bool + + // State vars + installed chan string + failures chan failedRun + done int + total int + waitingMessageIndex int + progress output.Progress + ticker *time.Ticker + tickInterval time.Duration + stats *installAnalytics +} + +func Install(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...Installer) error { + installer := newInstallManager(cmds, std.Out, parentEnv, verbose) + + installer.start(ctx) + + installer.install(ctx, cmds...) + + return installer.wait() +} + +func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, verbose bool) *InstallManager { + total := len(cmds) + return &InstallManager{ + out: out, + cmds: SliceToHashSet(cmds, func(c Installer) string { return c.GetName() }), + verbose: verbose, + env: env, + + installed: make(chan string, total), + failures: make(chan failedRun, total), + done: 0, + total: total, + } +} + +// starts all progress bars and counters but does not start installation +func (installer *InstallManager) start(ctx context.Context) { + installer.out.Write("") + installer.out.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", installer.total)) + installer.out.Write("") + + installer.progress = std.Out.Progress([]output.ProgressBar{ + {Label: fmt.Sprintf("Installing %d commands", installer.total), Max: float64(installer.total)}, + }, nil) + + // Every uninterrupted 15 seconds we will print out a waiting message + installer.startTicker(15 * time.Second) + + installer.startAnalytics(ctx, installer.cmds) +} + +// Starts the installation process in a non-blocking process +func (installer *InstallManager) install(ctx context.Context, cmds ...Installer) { + for _, cmd := range cmds { + go func(ctx context.Context, cmd Installer) { + if err := cmd.RunInstall(ctx, installer.env); err != nil { + // if failed, put on the failure queue and exit + installer.failures <- failedRun{cmdName: cmd.GetName(), err: err} + } + + installer.installed <- cmd.GetName() + }(ctx, cmd) + } +} + +// Blocks until all installations have successfully completed +// or until a failure occurs +func (installer *InstallManager) wait() error { + defer close(installer.installed) + defer close(installer.failures) + for { + select { + case cmdName := <-installer.installed: + installer.handleInstalled(cmdName) + + // Everything installed! + if installer.isDone() { + installer.complete() + return nil + } + + case failure := <-installer.failures: + installer.handleFailure(failure.cmdName, failure.err) + return failure + + case <-installer.tick(): + installer.handleWaiting() + } + } +} +func (installer *InstallManager) startTicker(interval time.Duration) { + installer.ticker = time.NewTicker(interval) + installer.tickInterval = interval +} + +func (installer *InstallManager) startAnalytics(ctx context.Context, cmds map[string]struct{}) { + installer.stats = startInstallAnalytics(ctx, cmds) +} + +func (installer *InstallManager) handleInstalled(name string) { + installer.stats.handleInstalled(name) + installer.ticker.Reset(installer.tickInterval) + + delete(installer.cmds, name) + installer.done += 1 + + installer.progress.WriteLine(output.Styledf(output.StyleSuccess, "%s installed", name)) + installer.progress.SetValue(0, float64(installer.done)) + installer.progress.SetLabelAndRecalc(0, fmt.Sprintf("%d/%d commands installed", int(installer.done), int(installer.total))) +} + +func (installer *InstallManager) complete() { + installer.progress.Complete() + + installer.out.Write("") + if installer.verbose { + installer.out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Took %s. Booting up the system!", installer.stats.duration())) + } else { + installer.out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) + } + installer.out.Write("") +} + +func (installer *InstallManager) handleFailure(name string, err error) { + installer.progress.Destroy() + installer.stats.handleFailure(name, err) + printCmdError(installer.out.Output, name, err) +} + +func (installer *InstallManager) handleWaiting() { + names := []string{} + for name := range installer.cmds { + names = append(names, name) + } + + msg := waitingMessages[installer.waitingMessageIndex] + emoji := output.EmojiHourglass + if installer.waitingMessageIndex > 3 { + emoji = output.EmojiShrug + } + + installer.progress.WriteLine(output.Linef(emoji, output.StyleBold, msg, strings.Join(names, ", "))) + installer.waitingMessageIndex = (installer.waitingMessageIndex + 1) % len(waitingMessages) +} + +func (installer *InstallManager) tick() <-chan time.Time { + return installer.ticker.C +} + +func (installer *InstallManager) isDone() bool { + return len(installer.cmds) == 0 +} + +type installAnalytics struct { + Start time.Time + Spans map[string]*analytics.Span +} + +func startInstallAnalytics(ctx context.Context, cmds map[string]struct{}) *installAnalytics { + installer := &installAnalytics{ + Start: time.Now(), + Spans: make(map[string]*analytics.Span, len(cmds)), + } + + for cmd := range cmds { + _, installer.Spans[cmd] = analytics.StartSpan(ctx, fmt.Sprintf("install %s", cmd), "install_command") + } + + interrupt.Register(installer.handleInterrupt) + + return installer +} + +func (a *installAnalytics) handleInterrupt() { + for _, span := range a.Spans { + if span.IsRecording() { + span.Cancelled() + span.End() + } + } +} + +func (a *installAnalytics) handleInstalled(name string) { + a.Spans[name].Succeeded() + a.Spans[name].End() +} + +func (a *installAnalytics) handleFailure(name string, err error) { + a.Spans[name].RecordError("failed", err) + a.Spans[name].End() +} + +func (a *installAnalytics) duration() time.Duration { + return time.Since(a.Start) +} + +type HashSet[T comparable] map[T]struct{} + +func SliceToHashSet[R any, T comparable](slice []R, extract func(R) T) HashSet[T] { + set := make(HashSet[T], len(slice)) + for _, item := range slice { + set[extract(item)] = struct{}{} + } + return set +} + +var waitingMessages = []string{ + "Still waiting for %s to finish installing...", + "Yup, still waiting for %s to finish installing...", + "Here's the bad news: still waiting for %s to finish installing. The good news is that we finally have a chance to talk, no?", + "Still waiting for %s to finish installing...", + "Hey, %s, there's people waiting for you, pal", + "Sooooo, how are ya? Yeah, waiting. I hear you. Wish %s would hurry up.", + "I mean, what is %s even doing?", + "I now expect %s to mean 'producing a miracle' with 'installing'", + "Still waiting for %s to finish installing...", + "Before this I think the longest I ever had to wait was at Disneyland in '99, but %s is now #1", + "Still waiting for %s to finish installing...", + "At this point it could be anything - does your computer still have power? Come on, %s", + "Might as well check Slack. %s is taking its time...", + "In German there's a saying: ein guter Käse braucht seine Zeit - a good cheese needs its time. Maybe %s is cheese?", + "If %ss turns out to be cheese I'm gonna lose it. Hey, hurry up, will ya", + "Still waiting for %s to finish installing...", +} diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index 7a2ecdd445c7..79e0044b222f 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -6,409 +6,178 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" "runtime" "strings" - "sync" - "time" - "github.com/grafana/regexp" - "github.com/rjeczalik/notify" - "golang.org/x/sync/semaphore" + "github.com/sourcegraph/conc/pool" - "github.com/sourcegraph/sourcegraph/dev/sg/internal/analytics" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" - "github.com/sourcegraph/sourcegraph/dev/sg/interrupt" "github.com/sourcegraph/sourcegraph/dev/sg/root" "github.com/sourcegraph/sourcegraph/internal/download" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" ) -const MAX_CONCURRENT_BUILD_PROCS = 4 - -func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...Command) error { +func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...ConfigCommand) (err error) { if len(cmds) == 0 { - // Exit early if there are no commands to run. + // no Bazel commands so we return return nil } - - chs := make([]<-chan struct{}, 0, len(cmds)) - monitor := &changeMonitor{} - for _, cmd := range cmds { - chs = append(chs, monitor.register(cmd)) - } - - pathChanges, err := watch() - if err != nil { - return err - } - go monitor.run(pathChanges) + std.Out.WriteLine(output.Styled(output.StylePending, fmt.Sprintf("Starting %d cmds", len(cmds)))) repoRoot, err := root.RepositoryRoot() if err != nil { return err } - // binaries get installed to /.bin. If the binary is installed with go build, then go - // will create .bin directory. Some binaries (like docsite) get downloaded instead of built and therefore - // need the directory to exist before hand. - binDir := filepath.Join(repoRoot, ".bin") - if err := os.Mkdir(binDir, 0755); err != nil && !os.IsExist(err) { - return err + runner := cmdRunner{ + std.Out, + cmds, + repoRoot, + parentEnv, + verbose, } - wg := sync.WaitGroup{} - installSemaphore := semaphore.NewWeighted(MAX_CONCURRENT_BUILD_PROCS) - failures := make(chan failedRun, len(cmds)) - installed := make(chan string, len(cmds)) - okayToStart := make(chan struct{}) - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - runner := &cmdRunner{ - verbose: verbose, - installSemaphore: installSemaphore, - failures: failures, - installed: installed, - okayToStart: okayToStart, - repositoryRoot: repoRoot, - parentEnv: parentEnv, - } + return runner.run(ctx) +} + +func (runner *cmdRunner) run(ctx context.Context) error { + p := pool.New().WithContext(ctx).WithCancelOnError() + // Start each Bazel command concurrently + for _, cmd := range runner.cmds { + cmd := cmd + p.Go(func(ctx context.Context) error { + std.Out.WriteLine(output.Styledf(output.StylePending, "Running %s...", cmd.GetName())) - cmdNames := make(map[string]struct{}, len(cmds)) + // Start watching the commands dependencies + wantRestart, err := cmd.StartWatch(ctx) + if err != nil { + runner.Write("Failed to watch " + cmd.GetName()) + runner.printError(cmd, err) + return err + } - for i, cmd := range cmds { - cmdNames[cmd.Name] = struct{}{} + // start up the binary + sc, err := runner.start(ctx, cmd) + if err != nil { + runner.Write("Failed to start " + cmd.GetName()) + runner.printError(cmd, err) + return errors.Wrapf(err, "failed to start command %q", cmd.GetName()) + } + defer sc.cancel() - wg.Add(1) + // Wait forever until we're asked to stop or that restarting returns an error. + for { + + select { + // Handle context cancelled + case <-ctx.Done(): + runner.debug("context error" + cmd.GetName()) + return ctx.Err() + + // Handle process exit + case err := <-sc.ErrorChannel(): + // Exited on its own or errored + if err != nil { + runner.debug("Error channel " + cmd.GetName()) + return err + } + runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.GetName(), output.StyleReset)) + + // If we shouldn't restart when the process exits, return + if !cmd.GetContinueWatchOnExit() { + return nil + } - go func(cmd Command, ch <-chan struct{}) { - defer wg.Done() - var err error - for first := true; cmd.ContinueWatchOnExit || first; first = false { - if err = runner.runAndWatch(ctx, cmd, ch); err != nil { - if errors.Is(err, ctx.Err()) { // if error caused by context, terminate - return + // handle file watcher triggered + case <-wantRestart: + // If the command has an installer, re-run the install and determine if we should restart + runner.WriteLine(output.Styledf(output.StylePending, "Change detected. Reloading %s...", cmd.GetName())) + shouldRestart, err := runner.reinstall(ctx, cmd) + if err != nil { + runner.debug("reinstall failure: %s", cmd.GetName()) + runner.printError(cmd, err) + return err } - if cmd.ContinueWatchOnExit { - printCmdError(std.Out.Output, cmd.Name, err) - time.Sleep(time.Second * 10) // backoff + + if shouldRestart { + std.Out.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", cmd.GetName())) + sc.cancel() + sc, err = runner.start(ctx, cmd) + defer sc.cancel() + if err != nil { + runner.debug("restart failure " + cmd.GetName()) + return err + } } else { - failures <- failedRun{cmdName: cmd.Name, err: err} + std.Out.WriteLine(output.Styledf(output.StylePending, "Binary for %s did not change. Not restarting.", cmd.GetName())) } } } - if err != nil { - cancel() - } - }(cmd, chs[i]) - } - - err = runner.waitForInstallation(ctx, cmdNames) - if err != nil { - return err + }) } - if err := writePid(); err != nil { - return err - } - - wg.Wait() - - select { - case <-ctx.Done(): - printCmdError(std.Out.Output, "other", ctx.Err()) - return ctx.Err() - case failure := <-failures: - printCmdError(std.Out.Output, failure.cmdName, failure.err) - return failure - default: - return nil - } + err := p.Wait() + runner.Write("Completed all commands") + return err } type cmdRunner struct { - verbose bool - - installSemaphore *semaphore.Weighted - failures chan failedRun - installed chan string - okayToStart chan struct{} - + *std.Output + cmds []ConfigCommand repositoryRoot string parentEnv map[string]string + verbose bool } -func (c *cmdRunner) runAndWatch(ctx context.Context, cmd Command, reload <-chan struct{}) error { - printDebug := func(f string, args ...any) { - if !c.verbose { - return - } - message := fmt.Sprintf(f, args...) - std.Out.WriteLine(output.Styledf(output.StylePending, "%s[DEBUG] %s: %s %s", output.StyleBold, cmd.Name, output.StyleReset, message)) - } - - startedOnce := false - - var ( - md5hash string - md5changed bool - ) - - var wg sync.WaitGroup - var cancelFuncs []context.CancelFunc - - errs := make(chan error, 1) - defer func() { - wg.Wait() - close(errs) - }() - - for { - // Build it - if cmd.Install != "" || cmd.InstallFunc != "" { - install := func() (string, error) { - if err := c.installSemaphore.Acquire(ctx, 1); err != nil { - return "", errors.Wrap(err, "lockfiles semaphore") - } - defer c.installSemaphore.Release(1) - - if startedOnce { - std.Out.WriteLine(output.Styledf(output.StylePending, "Installing %s...", cmd.Name)) - } - if cmd.Install != "" && cmd.InstallFunc == "" { - return BashInRoot(ctx, cmd.Install, makeEnv(c.parentEnv, cmd.Env)) - } else if cmd.Install == "" && cmd.InstallFunc != "" { - fn, ok := installFuncs[cmd.InstallFunc] - if !ok { - return "", errors.Newf("no install func with name %q found", cmd.InstallFunc) - } - return "", fn(ctx, makeEnvMap(c.parentEnv, cmd.Env)) - } - - return "", nil - } - - cmdOut, err := install() - if err != nil { - if !startedOnce { - return installErr{cmdName: cmd.Name, output: cmdOut, originalErr: err} - } else { - printCmdError(std.Out.Output, cmd.Name, reinstallErr{cmdName: cmd.Name, output: cmdOut}) - // Now we wait for a reload signal before we start to build it again - <-reload - continue - } - } - - // clear this signal before starting - select { - case <-reload: - default: - } +func (runner *cmdRunner) printError(cmd ConfigCommand, err error) { + printCmdError(runner.Output.Output, cmd.GetName(), err) +} - if startedOnce { - std.Out.WriteLine(output.Styledf(output.StyleSuccess, "%sSuccessfully installed %s%s", output.StyleBold, cmd.Name, output.StyleReset)) - } +func (runner *cmdRunner) debug(msg string, args ...any) { + if runner.verbose { + message := fmt.Sprintf(msg, args...) + runner.WriteLine(output.Styledf(output.StylePending, "%s[DEBUG]: %s %s", output.StyleBold, output.StyleReset, message)) + } +} - if cmd.CheckBinary != "" { - newHash, err := md5HashFile(filepath.Join(c.repositoryRoot, cmd.CheckBinary)) - if err != nil { - return installErr{cmdName: cmd.Name, output: cmdOut, originalErr: err} - } +func (runner *cmdRunner) start(ctx context.Context, cmd ConfigCommand) (*startedCmd, error) { + return startConfigCmd(ctx, cmd, runner.repositoryRoot, runner.parentEnv) +} - md5changed = md5hash != newHash - md5hash = newHash +func (runner *cmdRunner) reinstall(ctx context.Context, cmd ConfigCommand) (bool, error) { + if installer, ok := cmd.(Installer); ok { + bin, err := cmd.GetBinaryLocation() + if err != nil { + noBinary := noBinaryError{} + // If the command doesn't have a CheckBinary, we just ignore it + if errors.As(err, &noBinary) { + return false, nil + } else { + return false, err } - - } - - if !startedOnce { - c.installed <- cmd.Name - <-c.okayToStart } - if cmd.CheckBinary == "" || md5changed { - for _, cancel := range cancelFuncs { - printDebug("Canceling previous process and waiting for it to exit...") - cancel() // Stop command - <-errs // Wait for exit - printDebug("Previous command exited") - } - cancelFuncs = nil - - // Run it - std.Out.WriteLine(output.Styledf(output.StylePending, "Running %s...", cmd.Name)) - - sc, err := startCmd(ctx, c.repositoryRoot, cmd, c.parentEnv) - if err != nil { - return err - } - defer sc.cancel() - - cancelFuncs = append(cancelFuncs, sc.cancel) - - wg.Add(1) - go func() { - defer wg.Done() - - err := sc.Wait() - - var e *exec.ExitError - if errors.As(err, &e) { - err = runErr{ - cmdName: cmd.Name, - exitCode: e.ExitCode(), - stderr: sc.CapturedStderr(), - stdout: sc.CapturedStdout(), - } - } - if err == nil && cmd.ContinueWatchOnExit { - std.Out.WriteLine(output.Styledf(output.StyleSuccess, "Command %s completed", cmd.Name)) - <-reload // on success, wait for next reload before restarting - errs <- nil - } else { - errs <- err - } - }() - - // TODO: We should probably only set this after N seconds (or when - // we're sure that the command has booted up -- maybe healthchecks?) - startedOnce = true - } else { - std.Out.WriteLine(output.Styled(output.StylePending, "Binary did not change. Not restarting.")) + oldHash, err := md5HashFile(bin) + if err != nil { + return false, err } - select { - case <-reload: - std.Out.WriteLine(output.Styledf(output.StylePending, "Change detected. Reloading %s...", cmd.Name)) - continue // Reinstall - - case err := <-errs: - // Exited on its own or errored - if err == nil { - std.Out.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.Name, output.StyleReset)) - } - return err + if err := installer.RunInstall(ctx, runner.parentEnv); err != nil { + printCmdError(std.Out.Output, cmd.GetName(), err) + return false, err } - } -} - -func (c *cmdRunner) waitForInstallation(ctx context.Context, cmdNames map[string]struct{}) error { - installationStart := time.Now() - installationSpans := make(map[string]*analytics.Span, len(cmdNames)) - for name := range cmdNames { - _, installationSpans[name] = analytics.StartSpan(ctx, fmt.Sprintf("install %s", name), "install_command") - } - interrupt.Register(func() { - for _, span := range installationSpans { - if span.IsRecording() { - span.Cancelled() - span.End() - } + newHash, err := md5HashFile(bin) + if err != nil { + return false, err } - }) - - std.Out.Write("") - std.Out.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", len(cmdNames))) - std.Out.Write("") - - waitingMessages := []string{ - "Still waiting for %s to finish installing...", - "Yup, still waiting for %s to finish installing...", - "Here's the bad news: still waiting for %s to finish installing. The good news is that we finally have a chance to talk, no?", - "Still waiting for %s to finish installing...", - "Hey, %s, there's people waiting for you, pal", - "Sooooo, how are ya? Yeah, waiting. I hear you. Wish %s would hurry up.", - "I mean, what is %s even doing?", - "I now expect %s to mean 'producing a miracle' with 'installing'", - "Still waiting for %s to finish installing...", - "Before this I think the longest I ever had to wait was at Disneyland in '99, but %s is now #1", - "Still waiting for %s to finish installing...", - "At this point it could be anything - does your computer still have power? Come on, %s", - "Might as well check Slack. %s is taking its time...", - "In German there's a saying: ein guter Käse braucht seine Zeit - a good cheese needs its time. Maybe %s is cheese?", - "If %ss turns out to be cheese I'm gonna lose it. Hey, hurry up, will ya", - "Still waiting for %s to finish installing...", - } - messageCount := 0 - - const tickInterval = 15 * time.Second - ticker := time.NewTicker(tickInterval) - - done := 0.0 - total := float64(len(cmdNames)) - progress := std.Out.Progress([]output.ProgressBar{ - {Label: fmt.Sprintf("Installing %d commands", len(cmdNames)), Max: total}, - }, nil) - - for { - select { - case cmdName := <-c.installed: - ticker.Reset(tickInterval) - - delete(cmdNames, cmdName) - done += 1.0 - installationSpans[cmdName].Succeeded() - installationSpans[cmdName].End() - - progress.WriteLine(output.Styledf(output.StyleSuccess, "%s installed", cmdName)) - - progress.SetValue(0, done) - progress.SetLabelAndRecalc(0, fmt.Sprintf("%d/%d commands installed", int(done), int(total))) - - // Everything installed! - if len(cmdNames) == 0 { - progress.Complete() - - duration := time.Since(installationStart) - - std.Out.Write("") - if c.verbose { - std.Out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Took %s. Booting up the system!", duration)) - } else { - std.Out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) - } - std.Out.Write("") - - close(c.okayToStart) - return nil - } - - case failure := <-c.failures: - progress.Destroy() - installationSpans[failure.cmdName].RecordError("failed", failure.err) - installationSpans[failure.cmdName].End() - - // Something went wrong with an installation, no need to wait for the others - printCmdError(std.Out.Output, failure.cmdName, failure.err) - return failure - case <-ticker.C: - names := []string{} - for name := range cmdNames { - names = append(names, name) - } - - idx := messageCount - if idx > len(waitingMessages)-1 { - idx = len(waitingMessages) - 1 - } - msg := waitingMessages[idx] - - emoji := output.EmojiHourglass - if idx > 3 { - emoji = output.EmojiShrug - } - - progress.WriteLine(output.Linef(emoji, output.StyleBold, msg, strings.Join(names, ", "))) - messageCount += 1 - } + return oldHash != newHash, nil } + // If there is no installer, then we always restart + return true, nil } // failedRun is returned by run when a command failed to run and run exits @@ -433,17 +202,6 @@ func (e installErr) Error() string { return fmt.Sprintf("install of %s failed: %s", e.cmdName, e.output) } -// reinstallErr is used internally by runWatch to print a message when a -// command failed to reinstall. -type reinstallErr struct { - cmdName string - output string -} - -func (e reinstallErr) Error() string { - return fmt.Sprintf("reinstalling %s failed: %s", e.cmdName, e.output) -} - // runErr is used internally by runWatch to print a message when a // command failed to reinstall. type runErr struct { @@ -473,9 +231,6 @@ func printCmdError(out *output.Output, cmdName string, err error) { } } cmdOut = e.output - case reinstallErr: - message = "Failed to rebuild " + cmdName - cmdOut = e.output case runErr: message = "Failed to run " + cmdName cmdOut = fmt.Sprintf("Exit code: %d\n\n", e.exitCode) @@ -641,131 +396,16 @@ func md5HashFile(filename string) (string, error) { return string(h.Sum(nil)), nil } -// -// - -type changeMonitor struct { - subscriptions []subscription -} - -type subscription struct { - cmd Command - ch chan struct{} -} - -func (m *changeMonitor) run(paths <-chan string) { - for path := range paths { - for _, sub := range m.subscriptions { - m.notify(sub, path) - } - } -} - -func (m *changeMonitor) notify(sub subscription, path string) { - found := false - for _, prefix := range sub.cmd.Watch { - if strings.HasPrefix(path, prefix) { - found = true - } - } - if !found { - return - } - - select { - case sub.ch <- struct{}{}: - default: - } -} - -func (m *changeMonitor) register(cmd Command) <-chan struct{} { - ch := make(chan struct{}) - m.subscriptions = append(m.subscriptions, subscription{cmd, ch}) - return ch -} - -// -// - -var watchIgnorePatterns = []*regexp.Regexp{ - regexp.MustCompile(`_test\.go$`), - regexp.MustCompile(`^.bin/`), - regexp.MustCompile(`^.git/`), - regexp.MustCompile(`^dev/`), - regexp.MustCompile(`^node_modules/`), -} - -func watch() (<-chan string, error) { - repoRoot, err := root.RepositoryRoot() - if err != nil { - return nil, err - } - - paths := make(chan string) - events := make(chan notify.EventInfo, 1) - - if err := notify.Watch(repoRoot+"/...", events, notify.All); err != nil { - return nil, err - } - - go func() { - defer close(events) - defer notify.Stop(events) - - outer: - for event := range events { - path := strings.TrimPrefix(strings.TrimPrefix(event.Path(), repoRoot), "/") - - for _, pattern := range watchIgnorePatterns { - if pattern.MatchString(path) { - continue outer - } - } - - paths <- path - } - }() - - return paths, nil -} - -func Test(ctx context.Context, cmd Command, args []string, parentEnv map[string]string) error { +func Test(ctx context.Context, cmd ConfigCommand, parentEnv map[string]string) error { repoRoot, err := root.RepositoryRoot() if err != nil { return err } - std.Out.WriteLine(output.Styledf(output.StylePending, "Starting testsuite %q.", cmd.Name)) - if len(args) != 0 { - std.Out.WriteLine(output.Styledf(output.StylePending, "\tAdditional arguments: %s", args)) - } - commandCtx, cancel := context.WithCancel(ctx) - defer cancel() - - cmdArgs := []string{cmd.Cmd} - if len(args) != 0 { - cmdArgs = append(cmdArgs, args...) - } else { - cmdArgs = append(cmdArgs, cmd.DefaultArgs) - } - - secretsEnv, err := getSecrets(ctx, cmd.Name, cmd.ExternalSecrets) + std.Out.WriteLine(output.Styledf(output.StylePending, "Starting testsuite %q.", cmd.GetName())) + sc, err := startConfigCmd(ctx, cmd, repoRoot, parentEnv) if err != nil { - std.Out.WriteLine(output.Styledf(output.StyleWarning, "[%s] %s %s", - cmd.Name, output.EmojiFailure, err.Error())) + printCmdError(std.Out.Output, cmd.GetName(), err) } - - if cmd.Preamble != "" { - std.Out.WriteLine(output.Styledf(output.StyleOrange, "[%s] %s %s", cmd.Name, output.EmojiInfo, cmd.Preamble)) - } - - c := exec.CommandContext(commandCtx, "bash", "-c", strings.Join(cmdArgs, " ")) - c.Dir = repoRoot - c.Env = makeEnv(parentEnv, secretsEnv, cmd.Env) - c.Stdout = os.Stdout - c.Stderr = os.Stderr - - std.Out.WriteLine(output.Styledf(output.StylePending, "Running %s in %q...", c, repoRoot)) - - return c.Run() + return sc.Wait() } diff --git a/dev/sg/internal/run/run_bazel.go b/dev/sg/internal/run/run_bazel.go deleted file mode 100644 index 76d226ba4588..000000000000 --- a/dev/sg/internal/run/run_bazel.go +++ /dev/null @@ -1,73 +0,0 @@ -package run - -import ( - "context" - "fmt" - "os/exec" - "strings" - - "github.com/sourcegraph/conc/pool" - - "github.com/sourcegraph/sourcegraph/dev/sg/root" -) - -func outputPath() ([]byte, error) { - // Get the output directory from Bazel, which varies depending on which OS - // we're running against. - cmd := exec.Command("bazel", "info", "output_path") - return cmd.Output() -} - -// binLocation returns the path on disk where Bazel is putting the binary -// associated with a given target. -func binLocation(target string) (string, error) { - baseOutput, err := outputPath() - if err != nil { - return "", err - } - // Trim "bazel-out" because the next bazel query will include it. - outputPath := strings.TrimSuffix(strings.TrimSpace(string(baseOutput)), "bazel-out") - - // Get the binary from the specific target. - cmd := exec.Command("bazel", "cquery", target, "--output=files") - baseOutput, err = cmd.Output() - if err != nil { - return "", err - } - binPath := strings.TrimSpace(string(baseOutput)) - - return fmt.Sprintf("%s%s", outputPath, binPath), nil -} - -func BazelCommands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...BazelCommand) error { - if len(cmds) == 0 { - // no Bazel commands so we return - return nil - } - - repoRoot, err := root.RepositoryRoot() - if err != nil { - return err - } - - var targets []string - for _, cmd := range cmds { - targets = append(targets, cmd.Target) - } - - ibazel := newIBazel(repoRoot, targets...) - - p := pool.New().WithContext(ctx).WithCancelOnError() - p.Go(func(ctx context.Context) error { - return ibazel.Start(ctx, repoRoot) - }) - - for _, bc := range cmds { - bc := bc - p.Go(func(ctx context.Context) error { - return bc.Start(ctx, repoRoot, parentEnv) - }) - } - - return p.Wait() -} diff --git a/dev/sg/sg_run.go b/dev/sg/sg_run.go index a30fe02916e5..0c2343d178b5 100644 --- a/dev/sg/sg_run.go +++ b/dev/sg/sg_run.go @@ -92,48 +92,36 @@ func runExec(ctx *cli.Context) error { return flag.ErrHelp } - var cmds []run.Command - var bcmds []run.BazelCommand + cmds := make([]run.ConfigCommand, 0, len(args)) for _, arg := range args { - if bazelCmd, okB := config.BazelCommands[arg]; okB && !legacy { - bcmds = append(bcmds, bazelCmd) - } else { - cmd, okC := config.Commands[arg] - if !okC && !okB { - std.Out.WriteLine(output.Styledf(output.StyleWarning, "ERROR: command %q not found :(", arg)) - return flag.ErrHelp - } + if bazelCmd, ok := config.BazelCommands[arg]; ok && !legacy { + cmds = append(cmds, bazelCmd) + } else if cmd, ok := config.Commands[arg]; ok { cmds = append(cmds, cmd) + } else { + std.Out.WriteLine(output.Styledf(output.StyleWarning, "ERROR: command %q not found :(", arg)) + return flag.ErrHelp } } if ctx.Bool("describe") { - // TODO Bazel commands for _, cmd := range cmds { out, err := yaml.Marshal(cmd) if err != nil { return err } - std.Out.WriteMarkdown(fmt.Sprintf("# %s\n\n```yaml\n%s\n```\n\n", cmd.Name, string(out))) + if err = std.Out.WriteMarkdown(fmt.Sprintf("# %s\n\n```yaml\n%s\n```\n\n", cmd.GetName(), string(out))); err != nil { + return err + } } return nil } - if !legacy { - // First we build everything once, to ensure all binaries are present. - if err := run.BazelBuild(ctx.Context, bcmds...); err != nil { - return err - } - } - p := pool.New().WithContext(ctx.Context).WithCancelOnError() p.Go(func(ctx context.Context) error { return run.Commands(ctx, config.Env, verbose, cmds...) }) - p.Go(func(ctx context.Context) error { - return run.BazelCommands(ctx, config.Env, verbose, bcmds...) - }) return p.Wait() } diff --git a/dev/sg/sg_start.go b/dev/sg/sg_start.go index f9faeeecd4ab..c50847fb0749 100644 --- a/dev/sg/sg_start.go +++ b/dev/sg/sg_start.go @@ -8,10 +8,8 @@ import ( "path/filepath" "sort" "strings" - "sync" "time" - "github.com/sourcegraph/conc/pool" sgrun "github.com/sourcegraph/run" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" @@ -178,8 +176,6 @@ func constructStartCmdLongHelp() string { return out.String() } -var sgOnce sync.Once - func startExec(ctx *cli.Context) error { config, err := getConfig() if err != nil { @@ -305,6 +301,66 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C return err } + repoRoot, err := root.RepositoryRoot() + if err != nil { + return err + } + + cmds, err := getCommands(set.Commands, set, conf.Commands) + if err != nil { + return err + } + + bcmds, err := getCommands(set.BazelCommands, set, conf.BazelCommands) + if err != nil { + return err + } + + if len(cmds) == 0 && len(bcmds) == 0 { + std.Out.WriteLine(output.Styled(output.StyleWarning, "WARNING: no commands to run")) + return nil + } + + levelOverrides := logLevelOverrides() + for _, cmd := range cmds { + enrichWithLogLevels(&cmd, levelOverrides) + } + + env := conf.Env + for k, v := range set.Env { + env[k] = v + } + + installers := make([]run.Installer, 0, len(cmds)+1) + for _, cmd := range cmds { + installers = append(installers, cmd) + } + + if len(bcmds) > 0 { + ibazel, err := run.NewIBazel(bcmds, repoRoot) + if err != nil { + return err + } + defer ibazel.Stop() + installers = append(installers, ibazel) + } + + if err := run.Install(ctx, env, verbose, installers...); err != nil { + return err + } + + configCmds := make([]run.ConfigCommand, 0, len(bcmds)+len(cmds)) + for _, cmd := range bcmds { + configCmds = append(configCmds, cmd) + } + + for _, cmd := range cmds { + configCmds = append(configCmds, cmd) + } + return run.Commands(ctx, env, verbose, configCmds...) +} + +func getCommands[T run.ConfigCommand](commands []string, set *sgconf.Commandset, conf map[string]T) ([]T, error) { exceptList := exceptServices.Value() exceptSet := make(map[string]interface{}, len(exceptList)) for _, svc := range exceptList { @@ -317,15 +373,15 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C onlySet[svc] = struct{}{} } - cmds := make([]run.Command, 0, len(set.Commands)) - for _, name := range set.Commands { - cmd, ok := conf.Commands[name] + cmds := make([]T, 0, len(commands)) + for _, name := range commands { + cmd, ok := conf[name] if !ok { - return errors.Errorf("command %q not found in commandset %q", name, set.Name) + return nil, errors.Errorf("command %q not found in commandset %q", name, set.Name) } if _, excluded := exceptSet[name]; excluded { - std.Out.WriteLine(output.Styledf(output.StylePending, "Skipping command %s since it's in --except.", cmd.Name)) + std.Out.WriteLine(output.Styledf(output.StylePending, "Skipping command %s since it's in --except.", cmd.GetName())) continue } @@ -336,50 +392,12 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C if _, inSet := onlySet[name]; inSet { cmds = append(cmds, cmd) } else { - std.Out.WriteLine(output.Styledf(output.StylePending, "Skipping command %s since it's not included in --only.", cmd.Name)) + std.Out.WriteLine(output.Styledf(output.StylePending, "Skipping command %s since it's not included in --only.", cmd.GetName())) } } } - - bcmds := make([]run.BazelCommand, 0, len(set.BazelCommands)) - for _, name := range set.BazelCommands { - bcmd, ok := conf.BazelCommands[name] - if !ok { - return errors.Errorf("command %q not found in commandset %q", name, set.Name) - } - - bcmds = append(bcmds, bcmd) - } - if len(cmds) == 0 && len(bcmds) == 0 { - std.Out.WriteLine(output.Styled(output.StyleWarning, "WARNING: no commands to run")) - return nil - } - - levelOverrides := logLevelOverrides() - for _, cmd := range cmds { - enrichWithLogLevels(&cmd, levelOverrides) - } - - env := conf.Env - for k, v := range set.Env { - env[k] = v - } - - // First we build everything once, to ensure all binaries are present. - if err := run.BazelBuild(ctx, bcmds...); err != nil { - return err - } - - p := pool.New().WithContext(ctx).WithCancelOnError() - p.Go(func(ctx context.Context) error { - return run.Commands(ctx, env, verbose, cmds...) - }) - p.Go(func(ctx context.Context) error { - return run.BazelCommands(ctx, env, verbose, bcmds...) - }) - - return p.Wait() + return cmds, nil } // logLevelOverrides builds a map of commands -> log level that should be overridden in the environment. diff --git a/dev/sg/sg_tests.go b/dev/sg/sg_tests.go index 1f1946de5b68..cb1a4368c27c 100644 --- a/dev/sg/sg_tests.go +++ b/dev/sg/sg_tests.go @@ -1,8 +1,10 @@ package main import ( + "context" "flag" "fmt" + "os/exec" "sort" "strings" @@ -71,7 +73,7 @@ func testExec(ctx *cli.Context) error { return flag.ErrHelp } - return run.Test(ctx.Context, cmd, args[1:], config.Env) + return run.Test(ctx.Context, newSGTestCommand(cmd, args[1:]), config.Env) } func constructTestCmdLongHelp() string { @@ -102,3 +104,28 @@ func constructTestCmdLongHelp() string { return out.String() } + +type sgTestCommand struct { + run.Command + args []string +} + +// Ovrrides the GetExec method with a custom implementation to construct the command +// using CLI-passed arguments +func (test sgTestCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { + cmdArgs := []string{test.Command.Cmd} + if len(test.args) != 0 { + cmdArgs = append(cmdArgs, test.args...) + } else { + cmdArgs = append(cmdArgs, test.Command.DefaultArgs) + } + + return exec.CommandContext(ctx, "bash", "-c", strings.Join(cmdArgs, " ")), nil +} + +func newSGTestCommand(cmd run.Command, args []string) sgTestCommand { + return sgTestCommand{ + Command: cmd, + args: args, + } +} diff --git a/go.mod b/go.mod index 7a4401b8922d..e9546f62de8e 100644 --- a/go.mod +++ b/go.mod @@ -355,6 +355,7 @@ require ( github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.11 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/onsi/gomega v1.27.8 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect @@ -388,6 +389,7 @@ require ( go.uber.org/goleak v1.2.1 // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) require ( diff --git a/go.sum b/go.sum index 64d6500c918f..2522b2a11a93 100644 --- a/go.sum +++ b/go.sum @@ -1405,6 +1405,8 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= diff --git a/internal/codeintel/uploads/internal/store/processing.go b/internal/codeintel/uploads/internal/store/processing.go index 0ad7fc4138da..f8b1fe237948 100644 --- a/internal/codeintel/uploads/internal/store/processing.go +++ b/internal/codeintel/uploads/internal/store/processing.go @@ -15,6 +15,8 @@ import ( dbworkerstore "github.com/sourcegraph/sourcegraph/internal/workerutil/dbworker/store" ) +// adds a comment + // InsertUpload inserts a new upload and returns its identifier. func (s *store) InsertUpload(ctx context.Context, upload shared.Upload) (id int, err error) { ctx, _, endObservation := s.operations.insertUpload.With(ctx, &err, observation.Args{}) diff --git a/lib/errors/filter.go b/lib/errors/filter.go index 6a6b4b26363a..a3adb2e90a3a 100644 --- a/lib/errors/filter.go +++ b/lib/errors/filter.go @@ -12,6 +12,7 @@ func Ignore(err error, pred ErrorPredicate) error { // If the error (or any wrapped error) is a multierror, // filter its children. var multi *multiError + println("CHANGE") if As(err, &multi) { filtered := multi.errs[:0] for _, childErr := range multi.errs { diff --git a/sg.config.yaml b/sg.config.yaml index 312e5ebfbdce..63f7b9b4d6a5 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -407,6 +407,8 @@ commands: web: description: Enterprise version of the web app cmd: pnpm --filter @sourcegraph/web dev + watch: + - client/web/dev install: | pnpm install pnpm run generate @@ -973,6 +975,8 @@ bazelCommands: target: //cmd/blobstore:blobstore searcher: target: //cmd/searcher + docsite: + target: //doc:serve syntax-highlighter: target: //docker-images/syntax-highlighter:syntect_server ignoreStdout: true @@ -988,6 +992,7 @@ bazelCommands: QUIET: 'true' frontend: description: Enterprise frontend + continueWatchOnExit: true target: //cmd/frontend precmd: | export SOURCEGRAPH_LICENSE_GENERATION_KEY=$(cat ../dev-private/enterprise/dev/test-license-generation-key.pem) @@ -1018,7 +1023,7 @@ bazelCommands: USE_ROCKSKIP: 'false' gitserver-template: &gitserver_bazel_template target: //cmd/gitserver - env: &gitserverenv + env: HOSTNAME: 127.0.0.1:3178 # This is only here to stay backwards-compatible with people's custom # `sg.config.overwrite.yaml` files @@ -1075,7 +1080,8 @@ commandsets: - bazelisk - ibazel bazelCommands: - - blobstore + # - blobstore + - docsite - frontend - worker - repo-updater @@ -1083,15 +1089,21 @@ commandsets: - gitserver-1 - searcher - symbols - - syntax-highlighter + # - syntax-highlighter commands: - web - - docsite + # - docsite - zoekt-index-0 - zoekt-index-1 - zoekt-web-0 - zoekt-web-1 - caddy + simple: + bazelCommands: + - worker + commands: + - web + - docsite # If you modify this command set, please consider also updating the dotcom runset. enterprise: &enterprise_set requiresDevPrivate: true From 0f47a014d2727f4436bc1839b05c878de1c8ff45 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 18 Jan 2024 20:58:04 -0800 Subject: [PATCH 02/17] cleanup --- cmd/frontend/main.go | 2 - cmd/worker/BUILD.bazel | 2 +- dev/bazel_configure_accept_changes.sh | 3 + dev/sg/internal/run/BUILD.bazel | 2 +- dev/sg/internal/run/bazel_command.go | 1 - dev/sg/internal/run/command.go | 2 +- dev/sg/internal/run/installer.go | 82 ++++++++++++-- dev/sg/internal/run/run.go | 107 ++++-------------- ...{config_command.go => sgconfig_command.go} | 7 +- dev/sg/sg_run.go | 2 +- dev/sg/sg_start.go | 4 +- .../uploads/internal/store/processing.go | 2 - lib/errors/filter.go | 1 - sg.config.yaml | 5 - 14 files changed, 111 insertions(+), 111 deletions(-) rename dev/sg/internal/run/{config_command.go => sgconfig_command.go} (92%) diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index 5f83c6222382..a744d44d7cd1 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -16,8 +16,6 @@ import ( func main() { sanitycheck.Pass() - println("SUP") - // os.Exit(10) if os.Getenv("WEB_BUILDER_DEV_SERVER") == "1" { assets.UseDevAssetsProvider() } diff --git a/cmd/worker/BUILD.bazel b/cmd/worker/BUILD.bazel index 11a7e2ceffb1..ed73e3e183a2 100644 --- a/cmd/worker/BUILD.bazel +++ b/cmd/worker/BUILD.bazel @@ -1,5 +1,5 @@ -load("@container_structure_test//:defs.bzl", "container_structure_test") load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") +load("@container_structure_test//:defs.bzl", "container_structure_test") load("@rules_oci//oci:defs.bzl", "oci_image", "oci_push", "oci_tarball") load("@rules_pkg//:pkg.bzl", "pkg_tar") load("//dev:oci_defs.bzl", "image_repository") diff --git a/dev/bazel_configure_accept_changes.sh b/dev/bazel_configure_accept_changes.sh index b6eb3fbf4cd3..b78f2d29032a 100755 --- a/dev/bazel_configure_accept_changes.sh +++ b/dev/bazel_configure_accept_changes.sh @@ -1,6 +1,9 @@ #! /bin/bash # Run bazel configure and if the error code is 110, exit with error code 0 +# This is because 110 means that configuration files were successfully +# Can be used by processes which want to run configuration as an auto-fix +# and expect a 0 exit code bazel configure exit_code=$? diff --git a/dev/sg/internal/run/BUILD.bazel b/dev/sg/internal/run/BUILD.bazel index 60940e789220..aeeba08d4b76 100644 --- a/dev/sg/internal/run/BUILD.bazel +++ b/dev/sg/internal/run/BUILD.bazel @@ -6,7 +6,6 @@ go_library( srcs = [ "bazel_command.go", "command.go", - "config_command.go", "helpers.go", "ibazel.go", "installer.go", @@ -14,6 +13,7 @@ go_library( "pid.go", "prefix_suffix_saver.go", "run.go", + "sgconfig_command.go", ], importpath = "github.com/sourcegraph/sourcegraph/dev/sg/internal/run", visibility = ["//dev/sg:__subpackages__"], diff --git a/dev/sg/internal/run/bazel_command.go b/dev/sg/internal/run/bazel_command.go index 924fc6ad1c17..ab8514be90da 100644 --- a/dev/sg/internal/run/bazel_command.go +++ b/dev/sg/internal/run/bazel_command.go @@ -97,7 +97,6 @@ func (bc BazelCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { if err != nil { return nil, err } - println("Binary location: " + binLocation + "\n") return exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, binLocation)), nil } diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 060711d3b322..027706f95fa5 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -306,7 +306,7 @@ func OpenUnixSocket() error { return err } -func startConfigCmd(ctx context.Context, cmd ConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { +func startSgCmd(ctx context.Context, cmd SGConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { exec, err := cmd.GetExec(ctx) if err != nil { return nil, err diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go index 951c0b2c331b..82d3b6c65d7e 100644 --- a/dev/sg/internal/run/installer.go +++ b/dev/sg/internal/run/installer.go @@ -3,12 +3,18 @@ package run import ( "context" "fmt" + "os" + "path/filepath" + "runtime" "strings" "time" "github.com/sourcegraph/sourcegraph/dev/sg/internal/analytics" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" "github.com/sourcegraph/sourcegraph/dev/sg/interrupt" + "github.com/sourcegraph/sourcegraph/dev/sg/root" + "github.com/sourcegraph/sourcegraph/internal/download" + "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" ) @@ -19,7 +25,7 @@ type Installer interface { type InstallManager struct { // Constructor commands - out *std.Output + *std.Output cmds map[string]struct{} env map[string]string verbose bool @@ -49,7 +55,7 @@ func Install(ctx context.Context, parentEnv map[string]string, verbose bool, cmd func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, verbose bool) *InstallManager { total := len(cmds) return &InstallManager{ - out: out, + Output: out, cmds: SliceToHashSet(cmds, func(c Installer) string { return c.GetName() }), verbose: verbose, env: env, @@ -63,9 +69,9 @@ func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, // starts all progress bars and counters but does not start installation func (installer *InstallManager) start(ctx context.Context) { - installer.out.Write("") - installer.out.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", installer.total)) - installer.out.Write("") + installer.Write("") + installer.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", installer.total)) + installer.Write("") installer.progress = std.Out.Progress([]output.ProgressBar{ {Label: fmt.Sprintf("Installing %d commands", installer.total), Max: float64(installer.total)}, @@ -140,19 +146,19 @@ func (installer *InstallManager) handleInstalled(name string) { func (installer *InstallManager) complete() { installer.progress.Complete() - installer.out.Write("") + installer.Write("") if installer.verbose { - installer.out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Took %s. Booting up the system!", installer.stats.duration())) + installer.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Took %s. Booting up the system!", installer.stats.duration())) } else { - installer.out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) + installer.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) } - installer.out.Write("") + installer.Write("") } func (installer *InstallManager) handleFailure(name string, err error) { installer.progress.Destroy() installer.stats.handleFailure(name, err) - printCmdError(installer.out.Output, name, err) + printCmdError(installer.Output.Output, name, err) } func (installer *InstallManager) handleWaiting() { @@ -232,6 +238,62 @@ func SliceToHashSet[R any, T comparable](slice []R, extract func(R) T) HashSet[T return set } +type installFunc func(context.Context, map[string]string) error + +var installFuncs = map[string]installFunc{ + "installCaddy": func(ctx context.Context, env map[string]string) error { + version := env["CADDY_VERSION"] + if version == "" { + return errors.New("could not find CADDY_VERSION in env") + } + + root, err := root.RepositoryRoot() + if err != nil { + return err + } + + var os string + switch runtime.GOOS { + case "linux": + os = "linux" + case "darwin": + os = "mac" + } + + archiveName := fmt.Sprintf("caddy_%s_%s_%s", version, os, runtime.GOARCH) + url := fmt.Sprintf("https://github.com/caddyserver/caddy/releases/download/v%s/%s.tar.gz", version, archiveName) + + target := filepath.Join(root, fmt.Sprintf(".bin/caddy_%s", version)) + + return download.ArchivedExecutable(ctx, url, target, "caddy") + }, + "installJaeger": func(ctx context.Context, env map[string]string) error { + version := env["JAEGER_VERSION"] + + // Make sure the data folder exists. + disk := env["JAEGER_DISK"] + if err := os.MkdirAll(disk, 0755); err != nil { + return err + } + + if version == "" { + return errors.New("could not find JAEGER_VERSION in env") + } + + root, err := root.RepositoryRoot() + if err != nil { + return err + } + + archiveName := fmt.Sprintf("jaeger-%s-%s-%s", version, runtime.GOOS, runtime.GOARCH) + url := fmt.Sprintf("https://github.com/jaegertracing/jaeger/releases/download/v%s/%s.tar.gz", version, archiveName) + + target := filepath.Join(root, fmt.Sprintf(".bin/jaeger-all-in-one-%s", version)) + + return download.ArchivedExecutable(ctx, url, target, fmt.Sprintf("%s/jaeger-all-in-one", archiveName)) + }, +} + var waitingMessages = []string{ "Still waiting for %s to finish installing...", "Yup, still waiting for %s to finish installing...", diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index 79e0044b222f..a3d868d95988 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -7,21 +7,19 @@ import ( "io" "os" "path/filepath" - "runtime" "strings" "github.com/sourcegraph/conc/pool" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" "github.com/sourcegraph/sourcegraph/dev/sg/root" - "github.com/sourcegraph/sourcegraph/internal/download" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" ) -func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...ConfigCommand) (err error) { +func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...SGConfigCommand) (err error) { if len(cmds) == 0 { - // no Bazel commands so we return + // Exit early if there are no commands to run. return nil } std.Out.WriteLine(output.Styled(output.StylePending, fmt.Sprintf("Starting %d cmds", len(cmds)))) @@ -31,6 +29,14 @@ func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cm return err } + // binaries get installed to /.bin. If the binary is installed with go build, then go + // will create .bin directory. Some binaries (like docsite) get downloaded instead of built and therefore + // need the directory to exist before hand. + binDir := filepath.Join(repoRoot, ".bin") + if err := os.Mkdir(binDir, 0755); err != nil && !os.IsExist(err) { + return err + } + runner := cmdRunner{ std.Out, cmds, @@ -44,7 +50,7 @@ func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cm func (runner *cmdRunner) run(ctx context.Context) error { p := pool.New().WithContext(ctx).WithCancelOnError() - // Start each Bazel command concurrently + // Start each command concurrently for _, cmd := range runner.cmds { cmd := cmd p.Go(func(ctx context.Context) error { @@ -53,7 +59,6 @@ func (runner *cmdRunner) run(ctx context.Context) error { // Start watching the commands dependencies wantRestart, err := cmd.StartWatch(ctx) if err != nil { - runner.Write("Failed to watch " + cmd.GetName()) runner.printError(cmd, err) return err } @@ -61,7 +66,6 @@ func (runner *cmdRunner) run(ctx context.Context) error { // start up the binary sc, err := runner.start(ctx, cmd) if err != nil { - runner.Write("Failed to start " + cmd.GetName()) runner.printError(cmd, err) return errors.Wrapf(err, "failed to start command %q", cmd.GetName()) } @@ -73,14 +77,12 @@ func (runner *cmdRunner) run(ctx context.Context) error { select { // Handle context cancelled case <-ctx.Done(): - runner.debug("context error" + cmd.GetName()) return ctx.Err() // Handle process exit case err := <-sc.ErrorChannel(): - // Exited on its own or errored + // If the process failed, we exit immedieatly if err != nil { - runner.debug("Error channel " + cmd.GetName()) return err } runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.GetName(), output.StyleReset)) @@ -96,42 +98,38 @@ func (runner *cmdRunner) run(ctx context.Context) error { runner.WriteLine(output.Styledf(output.StylePending, "Change detected. Reloading %s...", cmd.GetName())) shouldRestart, err := runner.reinstall(ctx, cmd) if err != nil { - runner.debug("reinstall failure: %s", cmd.GetName()) runner.printError(cmd, err) return err } if shouldRestart { - std.Out.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", cmd.GetName())) + runner.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", cmd.GetName())) sc.cancel() sc, err = runner.start(ctx, cmd) - defer sc.cancel() if err != nil { - runner.debug("restart failure " + cmd.GetName()) return err } + defer sc.cancel() } else { - std.Out.WriteLine(output.Styledf(output.StylePending, "Binary for %s did not change. Not restarting.", cmd.GetName())) + runner.WriteLine(output.Styledf(output.StylePending, "Binary for %s did not change. Not restarting.", cmd.GetName())) } } } }) } - err := p.Wait() - runner.Write("Completed all commands") - return err + return p.Wait() } type cmdRunner struct { *std.Output - cmds []ConfigCommand + cmds []SGConfigCommand repositoryRoot string parentEnv map[string]string verbose bool } -func (runner *cmdRunner) printError(cmd ConfigCommand, err error) { +func (runner *cmdRunner) printError(cmd SGConfigCommand, err error) { printCmdError(runner.Output.Output, cmd.GetName(), err) } @@ -142,17 +140,16 @@ func (runner *cmdRunner) debug(msg string, args ...any) { } } -func (runner *cmdRunner) start(ctx context.Context, cmd ConfigCommand) (*startedCmd, error) { - return startConfigCmd(ctx, cmd, runner.repositoryRoot, runner.parentEnv) +func (runner *cmdRunner) start(ctx context.Context, cmd SGConfigCommand) (*startedCmd, error) { + return startSgCmd(ctx, cmd, runner.repositoryRoot, runner.parentEnv) } -func (runner *cmdRunner) reinstall(ctx context.Context, cmd ConfigCommand) (bool, error) { +func (runner *cmdRunner) reinstall(ctx context.Context, cmd SGConfigCommand) (bool, error) { if installer, ok := cmd.(Installer); ok { bin, err := cmd.GetBinaryLocation() if err != nil { - noBinary := noBinaryError{} // If the command doesn't have a CheckBinary, we just ignore it - if errors.As(err, &noBinary) { + if errors.Is(err, noBinaryError{}) { return false, nil } else { return false, err @@ -269,62 +266,6 @@ func printCmdError(out *output.Output, cmdName string, err error) { } } -type installFunc func(context.Context, map[string]string) error - -var installFuncs = map[string]installFunc{ - "installCaddy": func(ctx context.Context, env map[string]string) error { - version := env["CADDY_VERSION"] - if version == "" { - return errors.New("could not find CADDY_VERSION in env") - } - - root, err := root.RepositoryRoot() - if err != nil { - return err - } - - var os string - switch runtime.GOOS { - case "linux": - os = "linux" - case "darwin": - os = "mac" - } - - archiveName := fmt.Sprintf("caddy_%s_%s_%s", version, os, runtime.GOARCH) - url := fmt.Sprintf("https://github.com/caddyserver/caddy/releases/download/v%s/%s.tar.gz", version, archiveName) - - target := filepath.Join(root, fmt.Sprintf(".bin/caddy_%s", version)) - - return download.ArchivedExecutable(ctx, url, target, "caddy") - }, - "installJaeger": func(ctx context.Context, env map[string]string) error { - version := env["JAEGER_VERSION"] - - // Make sure the data folder exists. - disk := env["JAEGER_DISK"] - if err := os.MkdirAll(disk, 0755); err != nil { - return err - } - - if version == "" { - return errors.New("could not find JAEGER_VERSION in env") - } - - root, err := root.RepositoryRoot() - if err != nil { - return err - } - - archiveName := fmt.Sprintf("jaeger-%s-%s-%s", version, runtime.GOOS, runtime.GOARCH) - url := fmt.Sprintf("https://github.com/jaegertracing/jaeger/releases/download/v%s/%s.tar.gz", version, archiveName) - - target := filepath.Join(root, fmt.Sprintf(".bin/jaeger-all-in-one-%s", version)) - - return download.ArchivedExecutable(ctx, url, target, fmt.Sprintf("%s/jaeger-all-in-one", archiveName)) - }, -} - // makeEnv merges environments starting from the left, meaning the first environment will be overriden by the second one, skipping // any key that has been explicitly defined in the current environment of this process. This enables users to manually overrides // environment variables explictly, i.e FOO=1 sg start will have FOO=1 set even if a command or commandset sets FOO. @@ -396,14 +337,14 @@ func md5HashFile(filename string) (string, error) { return string(h.Sum(nil)), nil } -func Test(ctx context.Context, cmd ConfigCommand, parentEnv map[string]string) error { +func Test(ctx context.Context, cmd SGConfigCommand, parentEnv map[string]string) error { repoRoot, err := root.RepositoryRoot() if err != nil { return err } std.Out.WriteLine(output.Styledf(output.StylePending, "Starting testsuite %q.", cmd.GetName())) - sc, err := startConfigCmd(ctx, cmd, repoRoot, parentEnv) + sc, err := startSgCmd(ctx, cmd, repoRoot, parentEnv) if err != nil { printCmdError(std.Out.Output, cmd.GetName(), err) } diff --git a/dev/sg/internal/run/config_command.go b/dev/sg/internal/run/sgconfig_command.go similarity index 92% rename from dev/sg/internal/run/config_command.go rename to dev/sg/internal/run/sgconfig_command.go index be9a2cc8660d..ddfda63ae0ee 100644 --- a/dev/sg/internal/run/config_command.go +++ b/dev/sg/internal/run/sgconfig_command.go @@ -10,7 +10,7 @@ import ( "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" ) -type ConfigCommand interface { +type SGConfigCommand interface { // Getters for common fields GetName() string GetContinueWatchOnExit() bool @@ -78,3 +78,8 @@ func (e noBinaryError) Wrap(err error) error { e.err = err return e } + +func (e noBinaryError) Is(target error) bool { + _, ok := target.(noBinaryError) + return ok +} diff --git a/dev/sg/sg_run.go b/dev/sg/sg_run.go index 0c2343d178b5..a6d52df6c5ab 100644 --- a/dev/sg/sg_run.go +++ b/dev/sg/sg_run.go @@ -92,7 +92,7 @@ func runExec(ctx *cli.Context) error { return flag.ErrHelp } - cmds := make([]run.ConfigCommand, 0, len(args)) + cmds := make([]run.SGConfigCommand, 0, len(args)) for _, arg := range args { if bazelCmd, ok := config.BazelCommands[arg]; ok && !legacy { cmds = append(cmds, bazelCmd) diff --git a/dev/sg/sg_start.go b/dev/sg/sg_start.go index c50847fb0749..8a69b5e0ecb3 100644 --- a/dev/sg/sg_start.go +++ b/dev/sg/sg_start.go @@ -349,7 +349,7 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C return err } - configCmds := make([]run.ConfigCommand, 0, len(bcmds)+len(cmds)) + configCmds := make([]run.SGConfigCommand, 0, len(bcmds)+len(cmds)) for _, cmd := range bcmds { configCmds = append(configCmds, cmd) } @@ -360,7 +360,7 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C return run.Commands(ctx, env, verbose, configCmds...) } -func getCommands[T run.ConfigCommand](commands []string, set *sgconf.Commandset, conf map[string]T) ([]T, error) { +func getCommands[T run.SGConfigCommand](commands []string, set *sgconf.Commandset, conf map[string]T) ([]T, error) { exceptList := exceptServices.Value() exceptSet := make(map[string]interface{}, len(exceptList)) for _, svc := range exceptList { diff --git a/internal/codeintel/uploads/internal/store/processing.go b/internal/codeintel/uploads/internal/store/processing.go index f8b1fe237948..0ad7fc4138da 100644 --- a/internal/codeintel/uploads/internal/store/processing.go +++ b/internal/codeintel/uploads/internal/store/processing.go @@ -15,8 +15,6 @@ import ( dbworkerstore "github.com/sourcegraph/sourcegraph/internal/workerutil/dbworker/store" ) -// adds a comment - // InsertUpload inserts a new upload and returns its identifier. func (s *store) InsertUpload(ctx context.Context, upload shared.Upload) (id int, err error) { ctx, _, endObservation := s.operations.insertUpload.With(ctx, &err, observation.Args{}) diff --git a/lib/errors/filter.go b/lib/errors/filter.go index a3adb2e90a3a..6a6b4b26363a 100644 --- a/lib/errors/filter.go +++ b/lib/errors/filter.go @@ -12,7 +12,6 @@ func Ignore(err error, pred ErrorPredicate) error { // If the error (or any wrapped error) is a multierror, // filter its children. var multi *multiError - println("CHANGE") if As(err, &multi) { filtered := multi.errs[:0] for _, childErr := range multi.errs { diff --git a/sg.config.yaml b/sg.config.yaml index 525d090424e9..ea831bddff6c 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -406,8 +406,6 @@ commands: web: description: Enterprise version of the web app cmd: pnpm --filter @sourcegraph/web dev - watch: - - client/web/dev install: | pnpm install pnpm run generate @@ -974,8 +972,6 @@ bazelCommands: target: //cmd/blobstore:blobstore searcher: target: //cmd/searcher - docsite: - target: //doc:serve syntax-highlighter: target: //docker-images/syntax-highlighter:syntect_server ignoreStdout: true @@ -991,7 +987,6 @@ bazelCommands: QUIET: 'true' frontend: description: Enterprise frontend - continueWatchOnExit: true target: //cmd/frontend precmd: | export SOURCEGRAPH_LICENSE_GENERATION_KEY=$(cat ../dev-private/enterprise/dev/test-license-generation-key.pem) From 1e0f25d69251d1ba5cf0f744b7056fcb99e99d7e Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Fri, 19 Jan 2024 13:31:35 -0800 Subject: [PATCH 03/17] added output buffering --- dev/sg/internal/run/command.go | 47 ++++++++++++++++++++++++++-------- dev/sg/internal/run/ibazel.go | 6 ++++- dev/sg/sg_start.go | 8 ++++-- lib/process/pipe.go | 20 +++++++-------- sg.config.yaml | 2 +- 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 027706f95fa5..639dbf3cdb88 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -224,7 +224,8 @@ type startedCmd struct { outEg *pool.ErrorPool result chan error - opts commandOptions + opts commandOptions + startOutput chan struct{} } func (sc *startedCmd) ErrorChannel() <-chan error { @@ -275,6 +276,17 @@ func (sc *startedCmd) CapturedStderr() string { return string(sc.stderrBuf.Bytes()) } +// Begins writing output to StdOut and StdErr if it was previously buffered +// Errors if command was unbuffered +func (sc *startedCmd) StartOutput() error { + if sc.opts.bufferOutput { + close(sc.startOutput) + return nil + } + + return errors.Newf("cannot start output on unbuffered command: %s", sc.opts.name) +} + func getSecrets(ctx context.Context, name string, extSecrets map[string]secrets.ExternalSecret) (map[string]string, error) { secretsEnv := map[string]string{} @@ -348,9 +360,10 @@ type commandOptions struct { func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { sc := &startedCmd{ - opts: opts, - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, + opts: opts, + stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, + stderrBuf: &prefixSuffixSaver{N: 32 << 10}, + startOutput: make(chan struct{}), } ctx, cancel := context.WithCancel(ctx) @@ -363,13 +376,20 @@ func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { sc.Cmd.Dir = opts.dir sc.Cmd.Env = opts.env - if !opts.bufferOutput { - if err := sc.connectOutput(ctx); err != nil { - return nil, err - } + if err := sc.connectOutput(ctx); err != nil { + sc.cancel() + return nil, err } - return sc, sc.Start() + if !sc.opts.bufferOutput { + close(sc.startOutput) + } + + if err := sc.Start(); err != nil { + sc.cancel() + return nil, err + } + return sc, nil } func (sc *startedCmd) connectOutput(ctx context.Context) error { @@ -398,7 +418,14 @@ func (sc *startedCmd) connectOutput(ctx context.Context) error { stderrWriter = io.MultiWriter(logger, sc.stderrBuf, sgConnLog) } - eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) + // Blocks output until startOutput is signaled + pipe := func(writer io.Writer, reader io.Reader) error { + <-sc.startOutput + return process.DefaultPipe(writer, reader) + + } + + eg, err := process.PipeProcessOutput(ctx, sc.Cmd, stdoutWriter, stderrWriter, pipe) if err != nil { return err } diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index ae0e14d1fba6..50110093a4fc 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -129,7 +129,7 @@ func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { func (ib *IBazel) getCommandOptions(ctx context.Context) commandOptions { return commandOptions{ - name: ib.GetName(), + name: "iBazel", exec: ib.GetExec(ctx), dir: ib.dir, // Don't output iBazel logs until initial build is complete @@ -145,6 +145,10 @@ func (ib *IBazel) Build(ctx context.Context) (err error) { return err } +func (ib *IBazel) StartOutput() error { + return ib.proc.StartOutput() +} + func (ib *IBazel) Stop() { os.RemoveAll(ib.eventsDir) ib.proc.cancel() diff --git a/dev/sg/sg_start.go b/dev/sg/sg_start.go index 8a69b5e0ecb3..a4204851c5c8 100644 --- a/dev/sg/sg_start.go +++ b/dev/sg/sg_start.go @@ -336,19 +336,23 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C installers = append(installers, cmd) } + var ibazel *run.IBazel if len(bcmds) > 0 { - ibazel, err := run.NewIBazel(bcmds, repoRoot) + ibazel, err = run.NewIBazel(bcmds, repoRoot) if err != nil { return err } defer ibazel.Stop() installers = append(installers, ibazel) } - if err := run.Install(ctx, env, verbose, installers...); err != nil { return err } + if ibazel != nil { + ibazel.StartOutput() + } + configCmds := make([]run.SGConfigCommand, 0, len(bcmds)+len(cmds)) for _, cmd := range bcmds { configCmds = append(configCmds, cmd) diff --git a/lib/process/pipe.go b/lib/process/pipe.go index a2a9b23b1ab8..2620590618c5 100644 --- a/lib/process/pipe.go +++ b/lib/process/pipe.go @@ -21,6 +21,15 @@ const maxTokenSize = 100 * 1024 * 1024 // 100mb type pipe func(w io.Writer, r io.Reader) error +func DefaultPipe(w io.Writer, r io.Reader) error { + _, err := io.Copy(w, r) + // We can ignore ErrClosed because we get that if a process crashes + if err != nil && !errors.Is(err, fs.ErrClosed) { + return err + } + return nil +} + type cmdPiper interface { StdoutPipe() (io.ReadCloser, error) StderrPipe() (io.ReadCloser, error) @@ -64,16 +73,7 @@ func PipeOutput(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.W // PipeOutputUnbuffered is the unbuffered version of PipeOutput and uses // io.Copy instead of piping output line-based to the output. func PipeOutputUnbuffered(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer) (*pool.ErrorPool, error) { - pipe := func(w io.Writer, r io.Reader) error { - _, err := io.Copy(w, r) - // We can ignore ErrClosed because we get that if a process crashes - if err != nil && !errors.Is(err, fs.ErrClosed) { - return err - } - return nil - } - - return PipeProcessOutput(ctx, c, stdoutWriter, stderrWriter, pipe) + return PipeProcessOutput(ctx, c, stdoutWriter, stderrWriter, DefaultPipe) } func PipeProcessOutput(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer, fn pipe) (*pool.ErrorPool, error) { diff --git a/sg.config.yaml b/sg.config.yaml index ea831bddff6c..6319b0b37f7f 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -1075,7 +1075,7 @@ commandsets: - ibazel bazelCommands: # - blobstore - - docsite + # - docsite - frontend - worker - repo-updater From 5a6a4b80ee0a089cd1c1bcc9a0f8810f649100c7 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Mon, 22 Jan 2024 16:31:14 -0800 Subject: [PATCH 04/17] fixed process exiting (I think) --- dev/sg/internal/run/bazel_command.go | 8 +- dev/sg/internal/run/command.go | 38 +++++-- dev/sg/internal/run/ibazel.go | 135 ++++++++++++++---------- dev/sg/internal/run/installer.go | 10 +- dev/sg/internal/run/run.go | 8 +- dev/sg/internal/run/sgconfig_command.go | 14 ++- dev/sg/sg_tests.go | 4 +- 7 files changed, 138 insertions(+), 79 deletions(-) diff --git a/dev/sg/internal/run/bazel_command.go b/dev/sg/internal/run/bazel_command.go index ab8514be90da..d9eeaf74af2c 100644 --- a/dev/sg/internal/run/bazel_command.go +++ b/dev/sg/internal/run/bazel_command.go @@ -6,6 +6,8 @@ import ( "os/exec" "strings" + "github.com/rjeczalik/notify" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" ) @@ -88,11 +90,13 @@ func (bc BazelCommand) StartWatch(ctx context.Context) (<-chan struct{}, error) if watchPaths, err := bc.watchPaths(); err != nil { return nil, err } else { - return WatchPaths(ctx, watchPaths) + // skip remove events as we don't care about files being removed, we only + // want to know when the binary has been rebuilt + return WatchPaths(ctx, watchPaths, notify.Remove) } } -func (bc BazelCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { +func (bc BazelCommand) GetExecCmd(ctx context.Context) (*exec.Cmd, error) { binLocation, err := bc.GetBinaryLocation() if err != nil { return nil, err diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 639dbf3cdb88..828e46446086 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -5,14 +5,15 @@ import ( "fmt" "io" "net" - "os" "os/exec" "path/filepath" + "syscall" "github.com/sourcegraph/conc/pool" "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/dev/sg/interrupt" "github.com/sourcegraph/sourcegraph/dev/sg/root" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" @@ -80,7 +81,7 @@ func (cmd Command) GetEnv() map[string]string { return cmd.Env } -func (cmd Command) GetExec(ctx context.Context) (*exec.Cmd, error) { +func (cmd Command) GetExecCmd(ctx context.Context) (*exec.Cmd, error) { return exec.CommandContext(ctx, "bash", "-c", cmd.Cmd), nil } @@ -124,7 +125,7 @@ func (cmd Command) functionInstall(ctx context.Context, parentEnv map[string]str return nil } -func (cmd Command) watchPaths() ([]string, error) { +func (cmd Command) getWatchPaths() ([]string, error) { root, err := root.RepositoryRoot() if err != nil { return nil, err @@ -139,7 +140,7 @@ func (cmd Command) watchPaths() ([]string, error) { } func (cmd Command) StartWatch(ctx context.Context) (<-chan struct{}, error) { - if watchPaths, err := cmd.watchPaths(); err != nil { + if watchPaths, err := cmd.getWatchPaths(); err != nil { return nil, err } else { return WatchPaths(ctx, watchPaths) @@ -232,7 +233,6 @@ func (sc *startedCmd) ErrorChannel() <-chan error { if sc.result == nil { sc.result = make(chan error) go func() { - defer close(sc.result) sc.result <- sc.Wait() }() } @@ -319,7 +319,7 @@ func OpenUnixSocket() error { } func startSgCmd(ctx context.Context, cmd SGConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { - exec, err := cmd.GetExec(ctx) + exec, err := cmd.GetExecCmd(ctx) if err != nil { return nil, err } @@ -368,14 +368,38 @@ func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { ctx, cancel := context.WithCancel(ctx) sc.cancel = func() { - sc.Cmd.Process.Signal(os.Interrupt) + // The default cancel function will use a SIGKILL (9) which does + // not allow processes to cleanup. If they have spawned child processes + // those child processes will be orphaned and continue running. + // SIGINT will instead gracefully shut down the process and child processes. + if sc.Cmd.Process != nil { + // We created a process group above which we kill here. + pgid, err := syscall.Getpgid(sc.Cmd.Process.Pid) + if err != nil { + // Ignore Errno 3 (No such process) as this means the process has already exited + if !errors.Is(err, syscall.Errno(0x3)) { + panic(errors.Wrapf(err, "failed to get process group ID for %s (PID %d)", sc.opts.name, sc.Cmd.Process.Pid)) + } + // note the minus sign + } else if err := syscall.Kill(-pgid, syscall.SIGINT); err != nil { + panic(errors.Wrapf(err, "failed kill process group ID %d for cmd %s ", pgid, sc.opts.name)) + } + } + cancel() } + // Register an interrput handler + interrupt.Register(sc.cancel) sc.Cmd = opts.exec sc.Cmd.Dir = opts.dir sc.Cmd.Env = opts.env + // This sets up a process group which we kill later. + // This allows us to ensure that any child processes are killed as well when this exits + // This will only work on POSIX systems + sc.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + if err := sc.connectOutput(ctx); err != nil { sc.cancel() return nil, err diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index 50110093a4fc..916978e226af 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -12,19 +12,39 @@ import ( "github.com/nxadm/tail" - "github.com/sourcegraph/conc/pool" - "github.com/sourcegraph/sourcegraph/lib/errors" ) type IBazel struct { targets []string - events chan iBazelEvent + handler *iBazelEventHandler eventsDir string dir string proc *startedCmd } +// returns a runner to interact with ibazel. +func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { + eventsDir, err := os.MkdirTemp("", "ibazel-events") + if err != nil { + return nil, err + } + + targets := make([]string, 0, len(cmds)) + for _, cmd := range cmds { + if !slices.Contains(targets, cmd.Target) { + targets = append(targets, cmd.Target) + } + } + + return &IBazel{ + targets: targets, + handler: newIBazelEventHandler(profileEventsFilePath(eventsDir)), + eventsDir: eventsDir, + dir: dir, + }, nil +} + func (ibazel *IBazel) GetName() string { return fmt.Sprintf("bazel targets (%s)", strings.Join(ibazel.targets, ", ")) } @@ -40,17 +60,13 @@ func (ibazel *IBazel) RunInstall(ctx context.Context, env map[string]string) err return err } - p := pool.New().WithContext(ctx).WithCancelOnError() - - p.Go(func(ctx context.Context) error { - return ibazel.Watch(ctx) - }) + go ibazel.handler.watch(ctx) // block until initial ibazel build is completed return ibazel.WaitForInitialBuild(ctx) } -func (ib *IBazel) GetExec(ctx context.Context) *exec.Cmd { +func (ib *IBazel) GetExecCmd(ctx context.Context) *exec.Cmd { // Writes iBazel events out to a log file. These are much easier to parse // than trying to understand the output directly profilePath := "--profile_dev=" + ib.profileEventsFilePath() @@ -60,35 +76,6 @@ func (ib *IBazel) GetExec(ctx context.Context) *exec.Cmd { return exec.CommandContext(ctx, "ibazel", args...) } -// returns a runner to interact with ibazel. -func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { - eventsDir, err := os.MkdirTemp("", "ibazel-events") - if err != nil { - return nil, err - } - eventsFile, err := os.Create(profileEventsFilePath(eventsDir)) - if err != nil { - return nil, err - } - if err = eventsFile.Close(); err != nil { - return nil, err - } - - targets := make([]string, 0, len(cmds)) - for _, cmd := range cmds { - if !slices.Contains(targets, cmd.Target) { - targets = append(targets, cmd.Target) - } - } - - return &IBazel{ - targets: targets, - events: make(chan iBazelEvent), - eventsDir: eventsDir, - dir: dir, - }, nil -} - func (ib *IBazel) profileEventsFilePath() string { return profileEventsFilePath(ib.eventsDir) } @@ -97,25 +84,9 @@ func profileEventsFilePath(eventsDir string) string { return path.Join(eventsDir, "profile.json") } -// Watch opens the provided profile.json and reads it as it is continuously written by iBazel -// Each time it sees a iBazel event log, it parses it and puts it on the events channel -func (ib *IBazel) Watch(ctx context.Context) error { - tail, err := tail.TailFile(ib.profileEventsFilePath(), tail.Config{Follow: true, ReOpen: true}) - if err != nil { - return err - } - for line := range tail.Lines { - var event iBazelEvent - if err := json.Unmarshal([]byte(line.Text), &event); err != nil { - return errors.Newf("failed to unmarshal event json: %s", err) - } - ib.events <- event - } - return nil -} - func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { - for event := range ib.events { + defer ib.handler.close() + for event := range ib.handler.events { if event.Type == buildDone { return nil } @@ -123,14 +94,13 @@ func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { return errors.Newf("initial ibazel build failed") } } - return nil } func (ib *IBazel) getCommandOptions(ctx context.Context) commandOptions { return commandOptions{ name: "iBazel", - exec: ib.GetExec(ctx), + exec: ib.GetExecCmd(ctx), dir: ib.dir, // Don't output iBazel logs until initial build is complete // as it will break the progress bar @@ -154,6 +124,55 @@ func (ib *IBazel) Stop() { ib.proc.cancel() } +type iBazelEventHandler struct { + events chan iBazelEvent + stop chan struct{} + filename string +} + +func newIBazelEventHandler(filename string) *iBazelEventHandler { + return &iBazelEventHandler{ + events: make(chan iBazelEvent), + stop: make(chan struct{}), + filename: filename, + } +} + +// Watch opens the provided profile.json and reads it as it is continuously written by iBazel +// Each time it sees a iBazel event log, it parses it and puts it on the events channel +// This is a blocking function +func (h *iBazelEventHandler) watch(ctx context.Context) { + _, cancel := context.WithCancelCause(ctx) + tail, err := tail.TailFile(h.filename, tail.Config{Follow: true}) + if err != nil { + cancel(err) + } + defer tail.Cleanup() + defer close(h.events) + defer close(h.stop) + + for { + select { + case line := <-tail.Lines: + var event iBazelEvent + if err := json.Unmarshal([]byte(line.Text), &event); err != nil { + cancel(errors.Newf("failed to unmarshal event json: %s", err)) + } + h.events <- event + case <-ctx.Done(): + cancel(ctx.Err()) + return + case <-h.stop: + return + } + + } +} + +func (h *iBazelEventHandler) close() { + h.stop <- struct{}{} +} + // Schema information at https://github.com/bazelbuild/bazel-watcher?tab=readme-ov-file#profiler-events type iBazelEvent struct { // common diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go index 82d3b6c65d7e..42adf71c1caa 100644 --- a/dev/sg/internal/run/installer.go +++ b/dev/sg/internal/run/installer.go @@ -49,7 +49,7 @@ func Install(ctx context.Context, parentEnv map[string]string, verbose bool, cmd installer.install(ctx, cmds...) - return installer.wait() + return installer.wait(ctx) } func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, verbose bool) *InstallManager { @@ -99,7 +99,7 @@ func (installer *InstallManager) install(ctx context.Context, cmds ...Installer) // Blocks until all installations have successfully completed // or until a failure occurs -func (installer *InstallManager) wait() error { +func (installer *InstallManager) wait(ctx context.Context) error { defer close(installer.installed) defer close(installer.failures) for { @@ -117,6 +117,10 @@ func (installer *InstallManager) wait() error { installer.handleFailure(failure.cmdName, failure.err) return failure + case <-ctx.Done(): + // Context was canceled, exit early + return ctx.Err() + case <-installer.tick(): installer.handleWaiting() } @@ -294,6 +298,7 @@ var installFuncs = map[string]installFunc{ }, } +// As per tradition, if you edit this file you must add a new waiting message var waitingMessages = []string{ "Still waiting for %s to finish installing...", "Yup, still waiting for %s to finish installing...", @@ -311,4 +316,5 @@ var waitingMessages = []string{ "In German there's a saying: ein guter Käse braucht seine Zeit - a good cheese needs its time. Maybe %s is cheese?", "If %ss turns out to be cheese I'm gonna lose it. Hey, hurry up, will ya", "Still waiting for %s to finish installing...", + "You're probably wondering why I've called %s here today...", } diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index a3d868d95988..5595bd2b6246 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -73,7 +73,6 @@ func (runner *cmdRunner) run(ctx context.Context) error { // Wait forever until we're asked to stop or that restarting returns an error. for { - select { // Handle context cancelled case <-ctx.Done(): @@ -81,11 +80,12 @@ func (runner *cmdRunner) run(ctx context.Context) error { // Handle process exit case err := <-sc.ErrorChannel(): - // If the process failed, we exit immedieatly + // If the process failed, we exit immediately if err != nil { return err } - runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.GetName(), output.StyleReset)) + + runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error: %v, %s", output.StyleBold, cmd.GetName(), err, output.StyleReset)) // If we shouldn't restart when the process exits, return if !cmd.GetContinueWatchOnExit() { @@ -243,7 +243,7 @@ func printCmdError(out *output.Output, cmdName string, err error) { } default: - message = fmt.Sprintf("Failed to run %s: %s", cmdName, err) + message = fmt.Sprintf("Failed to run %s: %#v", cmdName, err) } separator := strings.Repeat("-", 80) diff --git a/dev/sg/internal/run/sgconfig_command.go b/dev/sg/internal/run/sgconfig_command.go index ddfda63ae0ee..05e7e2a12b89 100644 --- a/dev/sg/internal/run/sgconfig_command.go +++ b/dev/sg/internal/run/sgconfig_command.go @@ -20,16 +20,20 @@ type SGConfigCommand interface { GetEnv() map[string]string GetBinaryLocation() (string, error) GetExternalSecrets() map[string]secrets.ExternalSecret - GetExec(context.Context) (*exec.Cmd, error) + GetExecCmd(context.Context) (*exec.Cmd, error) // Start a file watcher on the relevant filesystem sub-tree for this command StartWatch(context.Context) (<-chan struct{}, error) } -func WatchPaths(ctx context.Context, paths []string) (<-chan struct{}, error) { +func WatchPaths(ctx context.Context, paths []string, skipEvents ...notify.Event) (<-chan struct{}, error) { // Set up the watchers. restart := make(chan struct{}) events := make(chan notify.EventInfo, 1) + skip := make(HashSet[notify.Event], len(skipEvents)) + for _, event := range skipEvents { + skip[event] = struct{}{} + } // Do nothing if no watch paths are configured if len(paths) == 0 { @@ -51,8 +55,10 @@ func WatchPaths(ctx context.Context, paths []string) (<-chan struct{}, error) { select { case <-ctx.Done(): return - case <-events: - restart <- struct{}{} + case evt := <-events: + if _, shouldSkip := skip[evt.Event()]; !shouldSkip { + restart <- struct{}{} + } } } diff --git a/dev/sg/sg_tests.go b/dev/sg/sg_tests.go index cb1a4368c27c..06a95c7eb683 100644 --- a/dev/sg/sg_tests.go +++ b/dev/sg/sg_tests.go @@ -110,9 +110,9 @@ type sgTestCommand struct { args []string } -// Ovrrides the GetExec method with a custom implementation to construct the command +// Ovrrides the GetExecCmd method with a custom implementation to construct the command // using CLI-passed arguments -func (test sgTestCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { +func (test sgTestCommand) GetExecCmd(ctx context.Context) (*exec.Cmd, error) { cmdArgs := []string{test.Command.Cmd} if len(test.args) != 0 { cmdArgs = append(cmdArgs, test.args...) From 9de816cf396c31b157f2f9f1a67d75cc970830f7 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Tue, 23 Jan 2024 10:19:32 -0800 Subject: [PATCH 05/17] added bazel run targets --- dev/sg/internal/run/bazel_command.go | 20 ++++++++++++++++---- dev/sg/internal/run/command.go | 2 +- dev/sg/internal/run/ibazel.go | 2 +- dev/sg/internal/run/run.go | 17 +++++++++++++++-- sg.config.yaml | 4 +++- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/dev/sg/internal/run/bazel_command.go b/dev/sg/internal/run/bazel_command.go index d9eeaf74af2c..def1737266a2 100644 --- a/dev/sg/internal/run/bazel_command.go +++ b/dev/sg/internal/run/bazel_command.go @@ -27,6 +27,9 @@ type BazelCommand struct { // Preamble is a short and visible message, displayed when the command is launched. Preamble string `yaml:"preamble"` ExternalSecrets map[string]secrets.ExternalSecret `yaml:"external_secrets"` + + // RunTarget specifies a target that should be run via `bazel run $RunTarget` instead of directly executing the binary. + RunTarget string `yaml:"runTarget"` } func (bc BazelCommand) GetName() string { @@ -77,6 +80,10 @@ func (bc BazelCommand) GetExternalSecrets() map[string]secrets.ExternalSecret { } func (bc BazelCommand) watchPaths() ([]string, error) { + // If no target is defined, there is nothing to be built and watched + if bc.Target == "" { + return nil, nil + } // Grab the location of the binary in bazel-out. binLocation, err := bc.GetBinaryLocation() if err != nil { @@ -97,12 +104,17 @@ func (bc BazelCommand) StartWatch(ctx context.Context) (<-chan struct{}, error) } func (bc BazelCommand) GetExecCmd(ctx context.Context) (*exec.Cmd, error) { - binLocation, err := bc.GetBinaryLocation() - if err != nil { - return nil, err + var cmd string + var err error + if bc.RunTarget != "" { + cmd = "bazel run " + bc.RunTarget + } else { + if cmd, err = bc.GetBinaryLocation(); err != nil { + return nil, err + } } - return exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, binLocation)), nil + return exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, cmd)), nil } func outputPath() ([]byte, error) { diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 828e46446086..34bd01989503 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -380,7 +380,7 @@ func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { if !errors.Is(err, syscall.Errno(0x3)) { panic(errors.Wrapf(err, "failed to get process group ID for %s (PID %d)", sc.opts.name, sc.Cmd.Process.Pid)) } - // note the minus sign + // note the minus sign; this signals that we want to kill the whole process group } else if err := syscall.Kill(-pgid, syscall.SIGINT); err != nil { panic(errors.Wrapf(err, "failed kill process group ID %d for cmd %s ", pgid, sc.opts.name)) } diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index 916978e226af..d2996ab6cca8 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -32,7 +32,7 @@ func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { targets := make([]string, 0, len(cmds)) for _, cmd := range cmds { - if !slices.Contains(targets, cmd.Target) { + if cmd.Target != "" && !slices.Contains(targets, cmd.Target) { targets = append(targets, cmd.Target) } } diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index 5595bd2b6246..2564c86a00b8 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "strings" @@ -85,7 +86,7 @@ func (runner *cmdRunner) run(ctx context.Context) error { return err } - runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error: %v, %s", output.StyleBold, cmd.GetName(), err, output.StyleReset)) + runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.GetName(), output.StyleReset)) // If we shouldn't restart when the process exits, return if !cmd.GetContinueWatchOnExit() { @@ -243,7 +244,19 @@ func printCmdError(out *output.Output, cmdName string, err error) { } default: - message = fmt.Sprintf("Failed to run %s: %#v", cmdName, err) + var exc *exec.ExitError + // recurse if it is an exit error + if errors.As(err, &exc) { + printCmdError(out, cmdName, runErr{ + cmdName: cmdName, + exitCode: exc.ExitCode(), + stderr: string(exc.Stderr), + }) + return + } else { + message = fmt.Sprintf("Failed to run %s: %#v", cmdName, err) + } + } separator := strings.Repeat("-", 80) diff --git a/sg.config.yaml b/sg.config.yaml index 6319b0b37f7f..95671ddff224 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -970,6 +970,8 @@ commands: bazelCommands: blobstore: target: //cmd/blobstore:blobstore + docsite: + runTarget: //doc:serve searcher: target: //cmd/searcher syntax-highlighter: @@ -1095,10 +1097,10 @@ commandsets: - caddy simple: bazelCommands: + - docsite - worker commands: - web - - docsite # If you modify this command set, please consider also updating the dotcom runset. enterprise: &enterprise_set requiresDevPrivate: true From a3f3ae512defe19ced704da5c24e54a73247573a Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Tue, 23 Jan 2024 11:07:27 -0800 Subject: [PATCH 06/17] ran go mod tidy and gazelle --- deps.bzl | 4 ++-- go.mod | 2 +- go.sum | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/deps.bzl b/deps.bzl index c5bf6043b0d9..6af4618a4118 100644 --- a/deps.bzl +++ b/deps.bzl @@ -4698,8 +4698,8 @@ def go_dependencies(): name = "com_github_nxadm_tail", build_file_proto_mode = "disable_global", importpath = "github.com/nxadm/tail", - sum = "h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=", - version = "v1.4.8", + sum = "h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=", + version = "v1.4.11", ) go_repository( name = "com_github_nytimes_gziphandler", diff --git a/go.mod b/go.mod index e5fe39b30ee7..b72da42075c9 100644 --- a/go.mod +++ b/go.mod @@ -267,6 +267,7 @@ require ( github.com/invopop/jsonschema v0.12.0 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/mroth/weightedrand/v2 v2.0.1 + github.com/nxadm/tail v1.4.11 github.com/oschwald/maxminddb-golang v1.12.0 github.com/pkoukk/tiktoken-go v0.1.6 github.com/prometheus/statsd_exporter v0.22.7 @@ -354,7 +355,6 @@ require ( github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nxadm/tail v1.4.11 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/onsi/gomega v1.27.8 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect diff --git a/go.sum b/go.sum index 0e8b4aaa4b2c..3140720cf71a 100644 --- a/go.sum +++ b/go.sum @@ -541,6 +541,7 @@ github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUork github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fullstorydev/grpcui v1.3.1 h1:lVXozTNkJJouBL+wpmvxMnltiwYp8mgyd0TRs93i6Rw= @@ -1403,7 +1404,6 @@ github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnu github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= @@ -2244,6 +2244,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 6b36048e09776697d283da5e034c7e275f1d3f19 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 18 Jan 2024 20:11:16 -0800 Subject: [PATCH 07/17] WIP draft --- .bazel_fix_commands.json | 18 +- cmd/frontend/main.go | 2 + cmd/worker/BUILD.bazel | 2 +- dev/bazel_configure_accept_changes.sh | 16 + dev/sg/internal/run/BUILD.bazel | 9 +- dev/sg/internal/run/bazel_build.go | 64 -- dev/sg/internal/run/bazel_command.go | 170 ++--- dev/sg/internal/run/command.go | 263 ++++++-- dev/sg/internal/run/config_command.go | 80 +++ dev/sg/internal/run/ibazel.go | 191 +++++- dev/sg/internal/run/installer.go | 252 ++++++++ dev/sg/internal/run/run.go | 606 ++++-------------- dev/sg/internal/run/run_bazel.go | 73 --- dev/sg/sg_run.go | 32 +- dev/sg/sg_start.go | 116 ++-- dev/sg/sg_tests.go | 29 +- go.mod | 2 + go.sum | 2 + .../uploads/internal/store/processing.go | 2 + lib/errors/filter.go | 1 + sg.config.yaml | 18 +- 21 files changed, 1058 insertions(+), 890 deletions(-) create mode 100755 dev/bazel_configure_accept_changes.sh delete mode 100644 dev/sg/internal/run/bazel_build.go create mode 100644 dev/sg/internal/run/config_command.go create mode 100644 dev/sg/internal/run/installer.go delete mode 100644 dev/sg/internal/run/run_bazel.go diff --git a/.bazel_fix_commands.json b/.bazel_fix_commands.json index fe51488c7066..b128df67c1f2 100644 --- a/.bazel_fix_commands.json +++ b/.bazel_fix_commands.json @@ -1 +1,17 @@ -[] +[ + { + "regex": "^Check that imports in Go sources match importpath attributes in deps.$", + "command": "./dev/bazel_configure_accept_changes.sh", + "args": [] + }, + { + "regex": "missing input file", + "command": "./dev/bazel_configure_accept_changes.sh", + "args": [] + }, + { + "regex": ": undefined:", + "command": "./dev/bazel_configure_accept_changes.sh", + "args": [] + } +] diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index a744d44d7cd1..5f83c6222382 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -16,6 +16,8 @@ import ( func main() { sanitycheck.Pass() + println("SUP") + // os.Exit(10) if os.Getenv("WEB_BUILDER_DEV_SERVER") == "1" { assets.UseDevAssetsProvider() } diff --git a/cmd/worker/BUILD.bazel b/cmd/worker/BUILD.bazel index ed73e3e183a2..11a7e2ceffb1 100644 --- a/cmd/worker/BUILD.bazel +++ b/cmd/worker/BUILD.bazel @@ -1,5 +1,5 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") load("@container_structure_test//:defs.bzl", "container_structure_test") +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") load("@rules_oci//oci:defs.bzl", "oci_image", "oci_push", "oci_tarball") load("@rules_pkg//:pkg.bzl", "pkg_tar") load("//dev:oci_defs.bzl", "image_repository") diff --git a/dev/bazel_configure_accept_changes.sh b/dev/bazel_configure_accept_changes.sh new file mode 100755 index 000000000000..b6eb3fbf4cd3 --- /dev/null +++ b/dev/bazel_configure_accept_changes.sh @@ -0,0 +1,16 @@ +#! /bin/bash + +# Run bazel configure and if the error code is 110, exit with error code 0 +bazel configure +exit_code=$? + +if [ $exit_code -eq 0 ]; then + echo "No configuration changes made" + exit 0 +elif [ $exit_code -eq 110 ]; then + echo "Bazel configuration completed" + exit 0 +else + echo "Unknown error" + exit $exit_code +fi diff --git a/dev/sg/internal/run/BUILD.bazel b/dev/sg/internal/run/BUILD.bazel index 08aa42ff93d4..60940e789220 100644 --- a/dev/sg/internal/run/BUILD.bazel +++ b/dev/sg/internal/run/BUILD.bazel @@ -1,19 +1,19 @@ -load("//dev:go_defs.bzl", "go_test") load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//dev:go_defs.bzl", "go_test") go_library( name = "run", srcs = [ - "bazel_build.go", "bazel_command.go", "command.go", + "config_command.go", "helpers.go", "ibazel.go", + "installer.go", "logger.go", "pid.go", "prefix_suffix_saver.go", "run.go", - "run_bazel.go", ], importpath = "github.com/sourcegraph/sourcegraph/dev/sg/internal/run", visibility = ["//dev/sg:__subpackages__"], @@ -27,10 +27,9 @@ go_library( "//lib/errors", "//lib/output", "//lib/process", - "@com_github_grafana_regexp//:regexp", + "@com_github_nxadm_tail//:tail", "@com_github_rjeczalik_notify//:notify", "@com_github_sourcegraph_conc//pool", - "@org_golang_x_sync//semaphore", ], ) diff --git a/dev/sg/internal/run/bazel_build.go b/dev/sg/internal/run/bazel_build.go deleted file mode 100644 index a258d8f59a00..000000000000 --- a/dev/sg/internal/run/bazel_build.go +++ /dev/null @@ -1,64 +0,0 @@ -package run - -import ( - "context" - "fmt" - "io" - "os/exec" - - "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" - "github.com/sourcegraph/sourcegraph/dev/sg/root" - "github.com/sourcegraph/sourcegraph/lib/output" - "github.com/sourcegraph/sourcegraph/lib/process" -) - -// BazelBuild peforms a bazel build command with all the given targets and blocks until an -// error is returned or the build is completed. -func BazelBuild(ctx context.Context, cmds ...BazelCommand) error { - if len(cmds) == 0 { - // no Bazel commands so we return - return nil - } - std.Out.WriteLine(output.Styled(output.StylePending, fmt.Sprintf("Detected %d bazel targets, running bazel build before anything else", len(cmds)))) - - repoRoot, err := root.RepositoryRoot() - if err != nil { - return err - } - - targets := make([]string, 0, len(cmds)) - for _, cmd := range cmds { - targets = append(targets, cmd.Target) - } - - var cancel func() - ctx, cancel = context.WithCancel(ctx) - - args := append([]string{"build"}, targets...) - cmd := exec.CommandContext(ctx, "bazel", args...) - - sc := &startedCmd{ - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, - } - - sc.cancel = cancel - sc.Cmd = cmd - sc.Cmd.Dir = repoRoot - - var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(ctx, "bazel", std.Out.Output) - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf) - stderrWriter = io.MultiWriter(logger, sc.stderrBuf) - eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) - if err != nil { - return err - } - sc.outEg = eg - - // Bazel out directory should exist here before returning - if err := sc.Start(); err != nil { - return err - } - return sc.Wait() -} diff --git a/dev/sg/internal/run/bazel_command.go b/dev/sg/internal/run/bazel_command.go index c3d07b744f9f..924fc6ad1c17 100644 --- a/dev/sg/internal/run/bazel_command.go +++ b/dev/sg/internal/run/bazel_command.go @@ -3,150 +3,108 @@ package run import ( "context" "fmt" - "io" "os/exec" + "strings" - "github.com/rjeczalik/notify" "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" - "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" - "github.com/sourcegraph/sourcegraph/lib/errors" - "github.com/sourcegraph/sourcegraph/lib/output" - "github.com/sourcegraph/sourcegraph/lib/process" ) // A BazelCommand is a command definition for sg run/start that uses // bazel under the hood. It will handle restarting itself autonomously, // as long as iBazel is running and watch that specific target. type BazelCommand struct { - Name string - Description string `yaml:"description"` - Target string `yaml:"target"` - Args string `yaml:"args"` - PreCmd string `yaml:"precmd"` - Env map[string]string `yaml:"env"` - IgnoreStdout bool `yaml:"ignoreStdout"` - IgnoreStderr bool `yaml:"ignoreStderr"` + Name string + Description string `yaml:"description"` + Target string `yaml:"target"` + Args string `yaml:"args"` + PreCmd string `yaml:"precmd"` + Env map[string]string `yaml:"env"` + IgnoreStdout bool `yaml:"ignoreStdout"` + IgnoreStderr bool `yaml:"ignoreStderr"` + ContinueWatchOnExit bool `yaml:"continueWatchOnExit"` + // Preamble is a short and visible message, displayed when the command is launched. + Preamble string `yaml:"preamble"` ExternalSecrets map[string]secrets.ExternalSecret `yaml:"external_secrets"` } -func (bc *BazelCommand) BinLocation() (string, error) { - return binLocation(bc.Target) +func (bc BazelCommand) GetName() string { + return bc.Name } -func (bc *BazelCommand) watch(ctx context.Context) (<-chan struct{}, error) { - // Grab the location of the binary in bazel-out. - binLocation, err := bc.BinLocation() - if err != nil { - return nil, err - } +func (bc BazelCommand) GetContinueWatchOnExit() bool { + return bc.ContinueWatchOnExit +} - // Set up the watcher. - restart := make(chan struct{}) - events := make(chan notify.EventInfo, 1) - if err := notify.Watch(binLocation, events, notify.All); err != nil { - return nil, err - } +func (bc BazelCommand) GetEnv() map[string]string { + return bc.Env +} - // Start watching for a freshly compiled version of the binary. - go func() { - defer close(events) - defer notify.Stop(events) - - for { - select { - case <-ctx.Done(): - return - case e := <-events: - if e.Event() != notify.Remove { - restart <- struct{}{} - } - } - - } - }() - - return restart, nil +func (bc BazelCommand) GetIgnoreStdout() bool { + return bc.IgnoreStdout } -func (bc *BazelCommand) Start(ctx context.Context, dir string, parentEnv map[string]string) error { - std.Out.WriteLine(output.Styledf(output.StylePending, "Running %s...", bc.Name)) +func (bc BazelCommand) GetIgnoreStderr() bool { + return bc.IgnoreStderr +} - // Run the binary for the first time. - cancel, err := bc.start(ctx, dir, parentEnv) - if err != nil { - return errors.Wrapf(err, "failed to start Bazel command %q", bc.Name) - } +func (bc BazelCommand) GetPreamble() string { + return bc.Preamble +} - // Restart when the binary change. - wantRestart, err := bc.watch(ctx) +func (bc BazelCommand) GetBinaryLocation() (string, error) { + baseOutput, err := outputPath() if err != nil { - return err + return "", err } + // Trim "bazel-out" because the next bazel query will include it. + outputPath := strings.TrimSuffix(strings.TrimSpace(string(baseOutput)), "bazel-out") - // Wait forever until we're asked to stop or that restarting returns an error. - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-wantRestart: - std.Out.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", bc.Name)) - cancel() - cancel, err = bc.start(ctx, dir, parentEnv) - if err != nil { - return err - } - } - } -} - -func (bc *BazelCommand) start(ctx context.Context, dir string, parentEnv map[string]string) (func(), error) { - binLocation, err := bc.BinLocation() + // Get the binary from the specific target. + cmd := exec.Command("bazel", "cquery", bc.Target, "--output=files") + baseOutput, err = cmd.Output() if err != nil { - return nil, err + return "", err } + binPath := strings.TrimSpace(string(baseOutput)) - sc := &startedCmd{ - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, - } + return fmt.Sprintf("%s%s", outputPath, binPath), nil +} - commandCtx, cancel := context.WithCancel(ctx) - sc.cancel = cancel - sc.Cmd = exec.CommandContext(commandCtx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, binLocation)) - sc.Cmd.Dir = dir +func (bc BazelCommand) GetExternalSecrets() map[string]secrets.ExternalSecret { + return bc.ExternalSecrets +} - secretsEnv, err := getSecrets(ctx, bc.Name, bc.ExternalSecrets) +func (bc BazelCommand) watchPaths() ([]string, error) { + // Grab the location of the binary in bazel-out. + binLocation, err := bc.GetBinaryLocation() if err != nil { - std.Out.WriteLine(output.Styledf(output.StyleWarning, "[%s] %s %s", - bc.Name, output.EmojiFailure, err.Error())) + return nil, err } + return []string{binLocation}, nil - sc.Cmd.Env = makeEnv(parentEnv, secretsEnv, bc.Env) +} - var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(commandCtx, bc.Name, std.Out.Output) - if bc.IgnoreStdout { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", bc.Name)) - stdoutWriter = sc.stdoutBuf - } else { - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf) - } - if bc.IgnoreStderr { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", bc.Name)) - stderrWriter = sc.stderrBuf +func (bc BazelCommand) StartWatch(ctx context.Context) (<-chan struct{}, error) { + if watchPaths, err := bc.watchPaths(); err != nil { + return nil, err } else { - stderrWriter = io.MultiWriter(logger, sc.stderrBuf) + return WatchPaths(ctx, watchPaths) } +} - eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) +func (bc BazelCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { + binLocation, err := bc.GetBinaryLocation() if err != nil { return nil, err } - sc.outEg = eg + println("Binary location: " + binLocation + "\n") - if err := sc.Start(); err != nil { - return nil, err - } + return exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, binLocation)), nil +} - return cancel, nil +func outputPath() ([]byte, error) { + // Get the output directory from Bazel, which varies depending on which OS + // we're running against. + cmd := exec.Command("bazel", "info", "output_path") + return cmd.Output() } diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 6926631aab99..060711d3b322 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -5,12 +5,15 @@ import ( "fmt" "io" "net" + "os" "os/exec" + "path/filepath" "github.com/sourcegraph/conc/pool" "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/dev/sg/root" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" "github.com/sourcegraph/sourcegraph/lib/process" @@ -38,6 +41,111 @@ type Command struct { // field in `Merge` (below). } +func (cmd Command) GetName() string { + return cmd.Name +} + +func (cmd Command) GetContinueWatchOnExit() bool { + return cmd.ContinueWatchOnExit +} + +func (cmd Command) GetBinaryLocation() (string, error) { + if cmd.CheckBinary != "" { + repoRoot, err := root.RepositoryRoot() + if err != nil { + return "", err + } + return filepath.Join(repoRoot, cmd.CheckBinary), nil + } + return "", noBinaryError{name: cmd.Name} +} + +func (cmd Command) GetExternalSecrets() map[string]secrets.ExternalSecret { + return cmd.ExternalSecrets +} + +func (cmd Command) GetIgnoreStdout() bool { + return cmd.IgnoreStdout +} + +func (cmd Command) GetIgnoreStderr() bool { + return cmd.IgnoreStderr +} + +func (cmd Command) GetPreamble() string { + return cmd.Preamble +} + +func (cmd Command) GetEnv() map[string]string { + return cmd.Env +} + +func (cmd Command) GetExec(ctx context.Context) (*exec.Cmd, error) { + return exec.CommandContext(ctx, "bash", "-c", cmd.Cmd), nil +} + +func (cmd Command) RunInstall(ctx context.Context, parentEnv map[string]string) error { + if cmd.requiresInstall() { + if cmd.hasBashInstaller() { + return cmd.bashInstall(ctx, parentEnv) + } else { + return cmd.functionInstall(ctx, parentEnv) + } + } + + return nil +} + +func (cmd Command) requiresInstall() bool { + return cmd.Install != "" || cmd.InstallFunc != "" +} + +func (cmd Command) hasBashInstaller() bool { + return cmd.Install != "" || cmd.InstallFunc == "" +} + +func (cmd Command) bashInstall(ctx context.Context, parentEnv map[string]string) error { + output, err := BashInRoot(ctx, cmd.Install, makeEnv(parentEnv, cmd.Env)) + if err != nil { + return installErr{cmdName: cmd.Name, output: output, originalErr: err} + } + return nil +} + +func (cmd Command) functionInstall(ctx context.Context, parentEnv map[string]string) error { + fn, ok := installFuncs[cmd.InstallFunc] + if !ok { + return installErr{cmdName: cmd.Name, originalErr: errors.Newf("no install func with name %q found", cmd.InstallFunc)} + } + if err := fn(ctx, makeEnvMap(parentEnv, cmd.Env)); err != nil { + return installErr{cmdName: cmd.Name, originalErr: err} + } + + return nil +} + +func (cmd Command) watchPaths() ([]string, error) { + root, err := root.RepositoryRoot() + if err != nil { + return nil, err + } + + fullPaths := make([]string, len(cmd.Watch)) + for i, path := range cmd.Watch { + fullPaths[i] = filepath.Join(root, path) + } + + return fullPaths, nil +} + +func (cmd Command) StartWatch(ctx context.Context) (<-chan struct{}, error) { + if watchPaths, err := cmd.watchPaths(); err != nil { + return nil, err + } else { + return WatchPaths(ctx, watchPaths) + } +} + func (c Command) Merge(other Command) Command { merged := c @@ -113,16 +221,44 @@ type startedCmd struct { stdoutBuf *prefixSuffixSaver stderrBuf *prefixSuffixSaver - outEg *pool.ErrorPool + outEg *pool.ErrorPool + result chan error + + opts commandOptions +} + +func (sc *startedCmd) ErrorChannel() <-chan error { + if sc.result == nil { + sc.result = make(chan error) + go func() { + defer close(sc.result) + sc.result <- sc.Wait() + }() + } + return sc.result } func (sc *startedCmd) Wait() error { + err := sc.wait() + var e *exec.ExitError + if errors.As(err, &e) { + err = runErr{ + cmdName: sc.opts.name, + exitCode: e.ExitCode(), + stderr: sc.CapturedStderr(), + stdout: sc.CapturedStdout(), + } + } + + return err +} + +func (sc *startedCmd) wait() error { if err := sc.outEg.Wait(); err != nil { return err } return sc.Cmd.Wait() } - func (sc *startedCmd) CapturedStdout() string { if sc.stdoutBuf == nil { return "" @@ -170,76 +306,103 @@ func OpenUnixSocket() error { return err } -func startCmd(ctx context.Context, dir string, cmd Command, parentEnv map[string]string) (*startedCmd, error) { +func startConfigCmd(ctx context.Context, cmd ConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { + exec, err := cmd.GetExec(ctx) + if err != nil { + return nil, err + } + + secretsEnv, err := getSecrets(ctx, cmd.GetName(), cmd.GetExternalSecrets()) + if err != nil { + std.Out.WriteLine(output.Styledf(output.StyleWarning, "[%s] %s %s", + cmd.GetName(), output.EmojiFailure, err.Error())) + } + + opts := commandOptions{ + name: cmd.GetName(), + exec: exec, + env: makeEnv(parentEnv, secretsEnv, cmd.GetEnv()), + dir: dir, + ignoreStdOut: cmd.GetIgnoreStdout(), + ignoreStdErr: cmd.GetIgnoreStderr(), + } + + if cmd.GetPreamble() != "" { + std.Out.WriteLine(output.Styledf(output.StyleOrange, "[%s] %s %s", cmd.GetName(), output.EmojiInfo, cmd.GetPreamble())) + } + + return startCmd(ctx, opts) +} + +type commandOptions struct { + name string + exec *exec.Cmd + dir string + env []string + ignoreStdOut bool + ignoreStdErr bool + // when enabled, stdout/stderr will not be streamed to the loggers + // after the process is begun, only captured for later retrieval + bufferOutput bool +} + +func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { sc := &startedCmd{ + opts: opts, stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, stderrBuf: &prefixSuffixSaver{N: 32 << 10}, } - commandCtx, cancel := context.WithCancel(ctx) - sc.cancel = cancel + ctx, cancel := context.WithCancel(ctx) + sc.cancel = func() { + sc.Cmd.Process.Signal(os.Interrupt) + cancel() + } - sc.Cmd = exec.CommandContext(commandCtx, "bash", "-c", cmd.Cmd) - sc.Cmd.Dir = dir + sc.Cmd = opts.exec + sc.Cmd.Dir = opts.dir + sc.Cmd.Env = opts.env - secretsEnv, err := getSecrets(ctx, cmd.Name, cmd.ExternalSecrets) - if err != nil { - std.Out.WriteLine(output.Styledf(output.StyleWarning, "[%s] %s %s", - cmd.Name, output.EmojiFailure, err.Error())) + if !opts.bufferOutput { + if err := sc.connectOutput(ctx); err != nil { + return nil, err + } } - sc.Cmd.Env = makeEnv(parentEnv, secretsEnv, cmd.Env) + return sc, sc.Start() +} + +func (sc *startedCmd) connectOutput(ctx context.Context) error { var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(commandCtx, cmd.Name, std.Out.Output) + logger := newCmdLogger(ctx, sc.opts.name, std.Out.Output) - // TODO(JH) sgtail experiment going on, this is a bit ugly, that will do it - // for the demo day. + var sgConnLog io.Writer = io.Discard if sgConn != nil { sink := func(data string) { - sgConn.Write([]byte(fmt.Sprintf("%s: %s\n", cmd.Name, data))) + sgConn.Write([]byte(fmt.Sprintf("%s: %s\n", sc.opts.name, data))) } - sgConnLog := process.NewLogger(ctx, sink) + sgConnLog = process.NewLogger(ctx, sink) + } - if cmd.IgnoreStdout { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", cmd.Name)) - stdoutWriter = sc.stdoutBuf - } else { - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf, sgConnLog) - } - if cmd.IgnoreStderr { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", cmd.Name)) - stderrWriter = sc.stderrBuf - } else { - stderrWriter = io.MultiWriter(logger, sc.stderrBuf, sgConnLog) - } + if sc.opts.ignoreStdOut { + std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", sc.opts.name)) + stdoutWriter = sc.stdoutBuf } else { - if cmd.IgnoreStdout { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", cmd.Name)) - stdoutWriter = sc.stdoutBuf - } else { - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf) - } - if cmd.IgnoreStderr { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", cmd.Name)) - stderrWriter = sc.stderrBuf - } else { - stderrWriter = io.MultiWriter(logger, sc.stderrBuf) - } + stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf, sgConnLog) } - - if cmd.Preamble != "" { - std.Out.WriteLine(output.Styledf(output.StyleOrange, "[%s] %s %s", cmd.Name, output.EmojiInfo, cmd.Preamble)) + if sc.opts.ignoreStdErr { + std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", sc.opts.name)) + stderrWriter = sc.stderrBuf + } else { + stderrWriter = io.MultiWriter(logger, sc.stderrBuf, sgConnLog) } + eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) if err != nil { - return nil, err + return err } sc.outEg = eg - if err := sc.Start(); err != nil { - return sc, err - } - - return sc, nil + return nil } diff --git a/dev/sg/internal/run/config_command.go b/dev/sg/internal/run/config_command.go new file mode 100644 index 000000000000..be9a2cc8660d --- /dev/null +++ b/dev/sg/internal/run/config_command.go @@ -0,0 +1,80 @@ +package run + +import ( + "context" + "fmt" + "os/exec" + + "github.com/rjeczalik/notify" + + "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" +) + +type ConfigCommand interface { + // Getters for common fields + GetName() string + GetContinueWatchOnExit() bool + GetIgnoreStdout() bool + GetIgnoreStderr() bool + GetPreamble() string + GetEnv() map[string]string + GetBinaryLocation() (string, error) + GetExternalSecrets() map[string]secrets.ExternalSecret + GetExec(context.Context) (*exec.Cmd, error) + + // Start a file watcher on the relevant filesystem sub-tree for this command + StartWatch(context.Context) (<-chan struct{}, error) +} + +func WatchPaths(ctx context.Context, paths []string) (<-chan struct{}, error) { + // Set up the watchers. + restart := make(chan struct{}) + events := make(chan notify.EventInfo, 1) + + // Do nothing if no watch paths are configured + if len(paths) == 0 { + return restart, nil + } + + for _, path := range paths { + if err := notify.Watch(path, events, notify.All); err != nil { + return nil, err + } + } + + // Start watching for changes to the source tree + go func() { + defer close(events) + defer notify.Stop(events) + + for { + select { + case <-ctx.Done(): + return + case <-events: + restart <- struct{}{} + } + + } + }() + + return restart, nil +} + +type noBinaryError struct { + name string + err error +} + +func (e noBinaryError) Error() string { + return fmt.Sprintf("no-binary-error: %s has no binary", e.name) +} + +func (e noBinaryError) Unwrap() error { + return e.err +} + +func (e noBinaryError) Wrap(err error) error { + e.err = err + return e +} diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index ee4293eccf93..ae0e14d1fba6 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -2,56 +2,183 @@ package run import ( "context" - "io" + "encoding/json" + "fmt" + "os" "os/exec" + "path" + "slices" + "strings" - "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" - "github.com/sourcegraph/sourcegraph/lib/process" + "github.com/nxadm/tail" + + "github.com/sourcegraph/conc/pool" + + "github.com/sourcegraph/sourcegraph/lib/errors" ) type IBazel struct { - pwd string - targets []string - cancel func() + targets []string + events chan iBazelEvent + eventsDir string + dir string + proc *startedCmd } -// newIBazel returns a runner to interact with ibazel. -func newIBazel(pwd string, targets ...string) *IBazel { - return &IBazel{ - pwd: pwd, - targets: targets, +func (ibazel *IBazel) GetName() string { + return fmt.Sprintf("bazel targets (%s)", strings.Join(ibazel.targets, ", ")) +} + +func (ibazel *IBazel) RunInstall(ctx context.Context, env map[string]string) error { + if len(ibazel.targets) == 0 { + // no Bazel commands so we return + return nil + } + + err := ibazel.Build(ctx) + if err != nil { + return err } + + p := pool.New().WithContext(ctx).WithCancelOnError() + + p.Go(func(ctx context.Context) error { + return ibazel.Watch(ctx) + }) + + // block until initial ibazel build is completed + return ibazel.WaitForInitialBuild(ctx) } -func (ib *IBazel) Start(ctx context.Context, dir string) error { - args := append([]string{"build"}, ib.targets...) - ctx, ib.cancel = context.WithCancel(ctx) - cmd := exec.CommandContext(ctx, "ibazel", args...) +func (ib *IBazel) GetExec(ctx context.Context) *exec.Cmd { + // Writes iBazel events out to a log file. These are much easier to parse + // than trying to understand the output directly + profilePath := "--profile_dev=" + ib.profileEventsFilePath() + // This enables iBazel to try to apply the fixes from .bazel_fix_commands.json automatically + enableAutoFix := "--run_output_interactive=false" + args := append([]string{profilePath, enableAutoFix, "build"}, ib.targets...) + return exec.CommandContext(ctx, "ibazel", args...) +} - sc := &startedCmd{ - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, +// returns a runner to interact with ibazel. +func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { + eventsDir, err := os.MkdirTemp("", "ibazel-events") + if err != nil { + return nil, err + } + eventsFile, err := os.Create(profileEventsFilePath(eventsDir)) + if err != nil { + return nil, err + } + if err = eventsFile.Close(); err != nil { + return nil, err } - sc.cancel = ib.cancel - sc.Cmd = cmd - sc.Cmd.Dir = dir + targets := make([]string, 0, len(cmds)) + for _, cmd := range cmds { + if !slices.Contains(targets, cmd.Target) { + targets = append(targets, cmd.Target) + } + } + + return &IBazel{ + targets: targets, + events: make(chan iBazelEvent), + eventsDir: eventsDir, + dir: dir, + }, nil +} + +func (ib *IBazel) profileEventsFilePath() string { + return profileEventsFilePath(ib.eventsDir) +} - var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(ctx, "iBazel", std.Out.Output) - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf) - stderrWriter = io.MultiWriter(logger, sc.stderrBuf) - eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) +func profileEventsFilePath(eventsDir string) string { + return path.Join(eventsDir, "profile.json") +} + +// Watch opens the provided profile.json and reads it as it is continuously written by iBazel +// Each time it sees a iBazel event log, it parses it and puts it on the events channel +func (ib *IBazel) Watch(ctx context.Context) error { + tail, err := tail.TailFile(ib.profileEventsFilePath(), tail.Config{Follow: true, ReOpen: true}) if err != nil { return err } - sc.outEg = eg - - // Bazel out directory should exist here before returning - return sc.Start() + for line := range tail.Lines { + var event iBazelEvent + if err := json.Unmarshal([]byte(line.Text), &event); err != nil { + return errors.Newf("failed to unmarshal event json: %s", err) + } + ib.events <- event + } + return nil } -func (ib *IBazel) Stop() error { - ib.cancel() +func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { + for event := range ib.events { + if event.Type == buildDone { + return nil + } + if event.Type == buildFailed { + return errors.Newf("initial ibazel build failed") + } + } + return nil } + +func (ib *IBazel) getCommandOptions(ctx context.Context) commandOptions { + return commandOptions{ + name: ib.GetName(), + exec: ib.GetExec(ctx), + dir: ib.dir, + // Don't output iBazel logs until initial build is complete + // as it will break the progress bar + bufferOutput: true, + } +} + +// Build starts an ibazel process to build the targets provided in the constructor +// It runs perpetually, watching for file changes +func (ib *IBazel) Build(ctx context.Context) (err error) { + ib.proc, err = startCmd(ctx, ib.getCommandOptions(ctx)) + return err +} + +func (ib *IBazel) Stop() { + os.RemoveAll(ib.eventsDir) + ib.proc.cancel() +} + +// Schema information at https://github.com/bazelbuild/bazel-watcher?tab=readme-ov-file#profiler-events +type iBazelEvent struct { + // common + Type string `json:"type"` + Iteration string `json:"iteration"` + Time int64 `json:"time"` + Targets []string `json:"targets,omitempty"` + Elapsed int64 `json:"elapsed,omitempty"` + + // start event + IBazelVersion string `json:"iBazelVersion,omitempty"` + BazelVersion string `json:"bazelVersion,omitempty"` + MaxHeapSize string `json:"maxHeapSize,omitempty"` + CommittedHeapSize string `json:"committedHeapSize,omitempty"` + + // change event + Change string `json:"change,omitempty"` + + // build & reload event + Changes []string `json:"changes,omitempty"` + + // browser event + RemoteType string `json:"remoteType,omitempty"` + RemoteTime int64 `json:"remoteTime,omitempty"` + RemoteElapsed int64 `json:"remoteElapsed,omitempty"` + RemoteData string `json:"remoteData,omitempty"` +} + +const ( + buildDone = "BUILD_DONE" + buildFailed = "BUILD_FAILED" +) diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go new file mode 100644 index 000000000000..951c0b2c331b --- /dev/null +++ b/dev/sg/internal/run/installer.go @@ -0,0 +1,252 @@ +package run + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/sourcegraph/sourcegraph/dev/sg/internal/analytics" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/dev/sg/interrupt" + "github.com/sourcegraph/sourcegraph/lib/output" +) + +type Installer interface { + RunInstall(ctx context.Context, env map[string]string) error + GetName() string +} + +type InstallManager struct { + // Constructor commands + out *std.Output + cmds map[string]struct{} + env map[string]string + verbose bool + + // State vars + installed chan string + failures chan failedRun + done int + total int + waitingMessageIndex int + progress output.Progress + ticker *time.Ticker + tickInterval time.Duration + stats *installAnalytics +} + +func Install(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...Installer) error { + installer := newInstallManager(cmds, std.Out, parentEnv, verbose) + + installer.start(ctx) + + installer.install(ctx, cmds...) + + return installer.wait() +} + +func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, verbose bool) *InstallManager { + total := len(cmds) + return &InstallManager{ + out: out, + cmds: SliceToHashSet(cmds, func(c Installer) string { return c.GetName() }), + verbose: verbose, + env: env, + + installed: make(chan string, total), + failures: make(chan failedRun, total), + done: 0, + total: total, + } +} + +// starts all progress bars and counters but does not start installation +func (installer *InstallManager) start(ctx context.Context) { + installer.out.Write("") + installer.out.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", installer.total)) + installer.out.Write("") + + installer.progress = std.Out.Progress([]output.ProgressBar{ + {Label: fmt.Sprintf("Installing %d commands", installer.total), Max: float64(installer.total)}, + }, nil) + + // Every uninterrupted 15 seconds we will print out a waiting message + installer.startTicker(15 * time.Second) + + installer.startAnalytics(ctx, installer.cmds) +} + +// Starts the installation process in a non-blocking process +func (installer *InstallManager) install(ctx context.Context, cmds ...Installer) { + for _, cmd := range cmds { + go func(ctx context.Context, cmd Installer) { + if err := cmd.RunInstall(ctx, installer.env); err != nil { + // if failed, put on the failure queue and exit + installer.failures <- failedRun{cmdName: cmd.GetName(), err: err} + } + + installer.installed <- cmd.GetName() + }(ctx, cmd) + } +} + +// Blocks until all installations have successfully completed +// or until a failure occurs +func (installer *InstallManager) wait() error { + defer close(installer.installed) + defer close(installer.failures) + for { + select { + case cmdName := <-installer.installed: + installer.handleInstalled(cmdName) + + // Everything installed! + if installer.isDone() { + installer.complete() + return nil + } + + case failure := <-installer.failures: + installer.handleFailure(failure.cmdName, failure.err) + return failure + + case <-installer.tick(): + installer.handleWaiting() + } + } +} +func (installer *InstallManager) startTicker(interval time.Duration) { + installer.ticker = time.NewTicker(interval) + installer.tickInterval = interval +} + +func (installer *InstallManager) startAnalytics(ctx context.Context, cmds map[string]struct{}) { + installer.stats = startInstallAnalytics(ctx, cmds) +} + +func (installer *InstallManager) handleInstalled(name string) { + installer.stats.handleInstalled(name) + installer.ticker.Reset(installer.tickInterval) + + delete(installer.cmds, name) + installer.done += 1 + + installer.progress.WriteLine(output.Styledf(output.StyleSuccess, "%s installed", name)) + installer.progress.SetValue(0, float64(installer.done)) + installer.progress.SetLabelAndRecalc(0, fmt.Sprintf("%d/%d commands installed", int(installer.done), int(installer.total))) +} + +func (installer *InstallManager) complete() { + installer.progress.Complete() + + installer.out.Write("") + if installer.verbose { + installer.out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Took %s. Booting up the system!", installer.stats.duration())) + } else { + installer.out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) + } + installer.out.Write("") +} + +func (installer *InstallManager) handleFailure(name string, err error) { + installer.progress.Destroy() + installer.stats.handleFailure(name, err) + printCmdError(installer.out.Output, name, err) +} + +func (installer *InstallManager) handleWaiting() { + names := []string{} + for name := range installer.cmds { + names = append(names, name) + } + + msg := waitingMessages[installer.waitingMessageIndex] + emoji := output.EmojiHourglass + if installer.waitingMessageIndex > 3 { + emoji = output.EmojiShrug + } + + installer.progress.WriteLine(output.Linef(emoji, output.StyleBold, msg, strings.Join(names, ", "))) + installer.waitingMessageIndex = (installer.waitingMessageIndex + 1) % len(waitingMessages) +} + +func (installer *InstallManager) tick() <-chan time.Time { + return installer.ticker.C +} + +func (installer *InstallManager) isDone() bool { + return len(installer.cmds) == 0 +} + +type installAnalytics struct { + Start time.Time + Spans map[string]*analytics.Span +} + +func startInstallAnalytics(ctx context.Context, cmds map[string]struct{}) *installAnalytics { + installer := &installAnalytics{ + Start: time.Now(), + Spans: make(map[string]*analytics.Span, len(cmds)), + } + + for cmd := range cmds { + _, installer.Spans[cmd] = analytics.StartSpan(ctx, fmt.Sprintf("install %s", cmd), "install_command") + } + + interrupt.Register(installer.handleInterrupt) + + return installer +} + +func (a *installAnalytics) handleInterrupt() { + for _, span := range a.Spans { + if span.IsRecording() { + span.Cancelled() + span.End() + } + } +} + +func (a *installAnalytics) handleInstalled(name string) { + a.Spans[name].Succeeded() + a.Spans[name].End() +} + +func (a *installAnalytics) handleFailure(name string, err error) { + a.Spans[name].RecordError("failed", err) + a.Spans[name].End() +} + +func (a *installAnalytics) duration() time.Duration { + return time.Since(a.Start) +} + +type HashSet[T comparable] map[T]struct{} + +func SliceToHashSet[R any, T comparable](slice []R, extract func(R) T) HashSet[T] { + set := make(HashSet[T], len(slice)) + for _, item := range slice { + set[extract(item)] = struct{}{} + } + return set +} + +var waitingMessages = []string{ + "Still waiting for %s to finish installing...", + "Yup, still waiting for %s to finish installing...", + "Here's the bad news: still waiting for %s to finish installing. The good news is that we finally have a chance to talk, no?", + "Still waiting for %s to finish installing...", + "Hey, %s, there's people waiting for you, pal", + "Sooooo, how are ya? Yeah, waiting. I hear you. Wish %s would hurry up.", + "I mean, what is %s even doing?", + "I now expect %s to mean 'producing a miracle' with 'installing'", + "Still waiting for %s to finish installing...", + "Before this I think the longest I ever had to wait was at Disneyland in '99, but %s is now #1", + "Still waiting for %s to finish installing...", + "At this point it could be anything - does your computer still have power? Come on, %s", + "Might as well check Slack. %s is taking its time...", + "In German there's a saying: ein guter Käse braucht seine Zeit - a good cheese needs its time. Maybe %s is cheese?", + "If %ss turns out to be cheese I'm gonna lose it. Hey, hurry up, will ya", + "Still waiting for %s to finish installing...", +} diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index 7a2ecdd445c7..79e0044b222f 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -6,409 +6,178 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" "runtime" "strings" - "sync" - "time" - "github.com/grafana/regexp" - "github.com/rjeczalik/notify" - "golang.org/x/sync/semaphore" + "github.com/sourcegraph/conc/pool" - "github.com/sourcegraph/sourcegraph/dev/sg/internal/analytics" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" - "github.com/sourcegraph/sourcegraph/dev/sg/interrupt" "github.com/sourcegraph/sourcegraph/dev/sg/root" "github.com/sourcegraph/sourcegraph/internal/download" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" ) -const MAX_CONCURRENT_BUILD_PROCS = 4 - -func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...Command) error { +func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...ConfigCommand) (err error) { if len(cmds) == 0 { - // Exit early if there are no commands to run. + // no Bazel commands so we return return nil } - - chs := make([]<-chan struct{}, 0, len(cmds)) - monitor := &changeMonitor{} - for _, cmd := range cmds { - chs = append(chs, monitor.register(cmd)) - } - - pathChanges, err := watch() - if err != nil { - return err - } - go monitor.run(pathChanges) + std.Out.WriteLine(output.Styled(output.StylePending, fmt.Sprintf("Starting %d cmds", len(cmds)))) repoRoot, err := root.RepositoryRoot() if err != nil { return err } - // binaries get installed to /.bin. If the binary is installed with go build, then go - // will create .bin directory. Some binaries (like docsite) get downloaded instead of built and therefore - // need the directory to exist before hand. - binDir := filepath.Join(repoRoot, ".bin") - if err := os.Mkdir(binDir, 0755); err != nil && !os.IsExist(err) { - return err + runner := cmdRunner{ + std.Out, + cmds, + repoRoot, + parentEnv, + verbose, } - wg := sync.WaitGroup{} - installSemaphore := semaphore.NewWeighted(MAX_CONCURRENT_BUILD_PROCS) - failures := make(chan failedRun, len(cmds)) - installed := make(chan string, len(cmds)) - okayToStart := make(chan struct{}) - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - runner := &cmdRunner{ - verbose: verbose, - installSemaphore: installSemaphore, - failures: failures, - installed: installed, - okayToStart: okayToStart, - repositoryRoot: repoRoot, - parentEnv: parentEnv, - } + return runner.run(ctx) +} + +func (runner *cmdRunner) run(ctx context.Context) error { + p := pool.New().WithContext(ctx).WithCancelOnError() + // Start each Bazel command concurrently + for _, cmd := range runner.cmds { + cmd := cmd + p.Go(func(ctx context.Context) error { + std.Out.WriteLine(output.Styledf(output.StylePending, "Running %s...", cmd.GetName())) - cmdNames := make(map[string]struct{}, len(cmds)) + // Start watching the commands dependencies + wantRestart, err := cmd.StartWatch(ctx) + if err != nil { + runner.Write("Failed to watch " + cmd.GetName()) + runner.printError(cmd, err) + return err + } - for i, cmd := range cmds { - cmdNames[cmd.Name] = struct{}{} + // start up the binary + sc, err := runner.start(ctx, cmd) + if err != nil { + runner.Write("Failed to start " + cmd.GetName()) + runner.printError(cmd, err) + return errors.Wrapf(err, "failed to start command %q", cmd.GetName()) + } + defer sc.cancel() - wg.Add(1) + // Wait forever until we're asked to stop or that restarting returns an error. + for { + + select { + // Handle context cancelled + case <-ctx.Done(): + runner.debug("context error" + cmd.GetName()) + return ctx.Err() + + // Handle process exit + case err := <-sc.ErrorChannel(): + // Exited on its own or errored + if err != nil { + runner.debug("Error channel " + cmd.GetName()) + return err + } + runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.GetName(), output.StyleReset)) + + // If we shouldn't restart when the process exits, return + if !cmd.GetContinueWatchOnExit() { + return nil + } - go func(cmd Command, ch <-chan struct{}) { - defer wg.Done() - var err error - for first := true; cmd.ContinueWatchOnExit || first; first = false { - if err = runner.runAndWatch(ctx, cmd, ch); err != nil { - if errors.Is(err, ctx.Err()) { // if error caused by context, terminate - return + // handle file watcher triggered + case <-wantRestart: + // If the command has an installer, re-run the install and determine if we should restart + runner.WriteLine(output.Styledf(output.StylePending, "Change detected. Reloading %s...", cmd.GetName())) + shouldRestart, err := runner.reinstall(ctx, cmd) + if err != nil { + runner.debug("reinstall failure: %s", cmd.GetName()) + runner.printError(cmd, err) + return err } - if cmd.ContinueWatchOnExit { - printCmdError(std.Out.Output, cmd.Name, err) - time.Sleep(time.Second * 10) // backoff + + if shouldRestart { + std.Out.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", cmd.GetName())) + sc.cancel() + sc, err = runner.start(ctx, cmd) + defer sc.cancel() + if err != nil { + runner.debug("restart failure " + cmd.GetName()) + return err + } } else { - failures <- failedRun{cmdName: cmd.Name, err: err} + std.Out.WriteLine(output.Styledf(output.StylePending, "Binary for %s did not change. Not restarting.", cmd.GetName())) } } } - if err != nil { - cancel() - } - }(cmd, chs[i]) - } - - err = runner.waitForInstallation(ctx, cmdNames) - if err != nil { - return err + }) } - if err := writePid(); err != nil { - return err - } - - wg.Wait() - - select { - case <-ctx.Done(): - printCmdError(std.Out.Output, "other", ctx.Err()) - return ctx.Err() - case failure := <-failures: - printCmdError(std.Out.Output, failure.cmdName, failure.err) - return failure - default: - return nil - } + err := p.Wait() + runner.Write("Completed all commands") + return err } type cmdRunner struct { - verbose bool - - installSemaphore *semaphore.Weighted - failures chan failedRun - installed chan string - okayToStart chan struct{} - + *std.Output + cmds []ConfigCommand repositoryRoot string parentEnv map[string]string + verbose bool } -func (c *cmdRunner) runAndWatch(ctx context.Context, cmd Command, reload <-chan struct{}) error { - printDebug := func(f string, args ...any) { - if !c.verbose { - return - } - message := fmt.Sprintf(f, args...) - std.Out.WriteLine(output.Styledf(output.StylePending, "%s[DEBUG] %s: %s %s", output.StyleBold, cmd.Name, output.StyleReset, message)) - } - - startedOnce := false - - var ( - md5hash string - md5changed bool - ) - - var wg sync.WaitGroup - var cancelFuncs []context.CancelFunc - - errs := make(chan error, 1) - defer func() { - wg.Wait() - close(errs) - }() - - for { - // Build it - if cmd.Install != "" || cmd.InstallFunc != "" { - install := func() (string, error) { - if err := c.installSemaphore.Acquire(ctx, 1); err != nil { - return "", errors.Wrap(err, "lockfiles semaphore") - } - defer c.installSemaphore.Release(1) - - if startedOnce { - std.Out.WriteLine(output.Styledf(output.StylePending, "Installing %s...", cmd.Name)) - } - if cmd.Install != "" && cmd.InstallFunc == "" { - return BashInRoot(ctx, cmd.Install, makeEnv(c.parentEnv, cmd.Env)) - } else if cmd.Install == "" && cmd.InstallFunc != "" { - fn, ok := installFuncs[cmd.InstallFunc] - if !ok { - return "", errors.Newf("no install func with name %q found", cmd.InstallFunc) - } - return "", fn(ctx, makeEnvMap(c.parentEnv, cmd.Env)) - } - - return "", nil - } - - cmdOut, err := install() - if err != nil { - if !startedOnce { - return installErr{cmdName: cmd.Name, output: cmdOut, originalErr: err} - } else { - printCmdError(std.Out.Output, cmd.Name, reinstallErr{cmdName: cmd.Name, output: cmdOut}) - // Now we wait for a reload signal before we start to build it again - <-reload - continue - } - } - - // clear this signal before starting - select { - case <-reload: - default: - } +func (runner *cmdRunner) printError(cmd ConfigCommand, err error) { + printCmdError(runner.Output.Output, cmd.GetName(), err) +} - if startedOnce { - std.Out.WriteLine(output.Styledf(output.StyleSuccess, "%sSuccessfully installed %s%s", output.StyleBold, cmd.Name, output.StyleReset)) - } +func (runner *cmdRunner) debug(msg string, args ...any) { + if runner.verbose { + message := fmt.Sprintf(msg, args...) + runner.WriteLine(output.Styledf(output.StylePending, "%s[DEBUG]: %s %s", output.StyleBold, output.StyleReset, message)) + } +} - if cmd.CheckBinary != "" { - newHash, err := md5HashFile(filepath.Join(c.repositoryRoot, cmd.CheckBinary)) - if err != nil { - return installErr{cmdName: cmd.Name, output: cmdOut, originalErr: err} - } +func (runner *cmdRunner) start(ctx context.Context, cmd ConfigCommand) (*startedCmd, error) { + return startConfigCmd(ctx, cmd, runner.repositoryRoot, runner.parentEnv) +} - md5changed = md5hash != newHash - md5hash = newHash +func (runner *cmdRunner) reinstall(ctx context.Context, cmd ConfigCommand) (bool, error) { + if installer, ok := cmd.(Installer); ok { + bin, err := cmd.GetBinaryLocation() + if err != nil { + noBinary := noBinaryError{} + // If the command doesn't have a CheckBinary, we just ignore it + if errors.As(err, &noBinary) { + return false, nil + } else { + return false, err } - - } - - if !startedOnce { - c.installed <- cmd.Name - <-c.okayToStart } - if cmd.CheckBinary == "" || md5changed { - for _, cancel := range cancelFuncs { - printDebug("Canceling previous process and waiting for it to exit...") - cancel() // Stop command - <-errs // Wait for exit - printDebug("Previous command exited") - } - cancelFuncs = nil - - // Run it - std.Out.WriteLine(output.Styledf(output.StylePending, "Running %s...", cmd.Name)) - - sc, err := startCmd(ctx, c.repositoryRoot, cmd, c.parentEnv) - if err != nil { - return err - } - defer sc.cancel() - - cancelFuncs = append(cancelFuncs, sc.cancel) - - wg.Add(1) - go func() { - defer wg.Done() - - err := sc.Wait() - - var e *exec.ExitError - if errors.As(err, &e) { - err = runErr{ - cmdName: cmd.Name, - exitCode: e.ExitCode(), - stderr: sc.CapturedStderr(), - stdout: sc.CapturedStdout(), - } - } - if err == nil && cmd.ContinueWatchOnExit { - std.Out.WriteLine(output.Styledf(output.StyleSuccess, "Command %s completed", cmd.Name)) - <-reload // on success, wait for next reload before restarting - errs <- nil - } else { - errs <- err - } - }() - - // TODO: We should probably only set this after N seconds (or when - // we're sure that the command has booted up -- maybe healthchecks?) - startedOnce = true - } else { - std.Out.WriteLine(output.Styled(output.StylePending, "Binary did not change. Not restarting.")) + oldHash, err := md5HashFile(bin) + if err != nil { + return false, err } - select { - case <-reload: - std.Out.WriteLine(output.Styledf(output.StylePending, "Change detected. Reloading %s...", cmd.Name)) - continue // Reinstall - - case err := <-errs: - // Exited on its own or errored - if err == nil { - std.Out.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.Name, output.StyleReset)) - } - return err + if err := installer.RunInstall(ctx, runner.parentEnv); err != nil { + printCmdError(std.Out.Output, cmd.GetName(), err) + return false, err } - } -} - -func (c *cmdRunner) waitForInstallation(ctx context.Context, cmdNames map[string]struct{}) error { - installationStart := time.Now() - installationSpans := make(map[string]*analytics.Span, len(cmdNames)) - for name := range cmdNames { - _, installationSpans[name] = analytics.StartSpan(ctx, fmt.Sprintf("install %s", name), "install_command") - } - interrupt.Register(func() { - for _, span := range installationSpans { - if span.IsRecording() { - span.Cancelled() - span.End() - } + newHash, err := md5HashFile(bin) + if err != nil { + return false, err } - }) - - std.Out.Write("") - std.Out.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", len(cmdNames))) - std.Out.Write("") - - waitingMessages := []string{ - "Still waiting for %s to finish installing...", - "Yup, still waiting for %s to finish installing...", - "Here's the bad news: still waiting for %s to finish installing. The good news is that we finally have a chance to talk, no?", - "Still waiting for %s to finish installing...", - "Hey, %s, there's people waiting for you, pal", - "Sooooo, how are ya? Yeah, waiting. I hear you. Wish %s would hurry up.", - "I mean, what is %s even doing?", - "I now expect %s to mean 'producing a miracle' with 'installing'", - "Still waiting for %s to finish installing...", - "Before this I think the longest I ever had to wait was at Disneyland in '99, but %s is now #1", - "Still waiting for %s to finish installing...", - "At this point it could be anything - does your computer still have power? Come on, %s", - "Might as well check Slack. %s is taking its time...", - "In German there's a saying: ein guter Käse braucht seine Zeit - a good cheese needs its time. Maybe %s is cheese?", - "If %ss turns out to be cheese I'm gonna lose it. Hey, hurry up, will ya", - "Still waiting for %s to finish installing...", - } - messageCount := 0 - - const tickInterval = 15 * time.Second - ticker := time.NewTicker(tickInterval) - - done := 0.0 - total := float64(len(cmdNames)) - progress := std.Out.Progress([]output.ProgressBar{ - {Label: fmt.Sprintf("Installing %d commands", len(cmdNames)), Max: total}, - }, nil) - - for { - select { - case cmdName := <-c.installed: - ticker.Reset(tickInterval) - - delete(cmdNames, cmdName) - done += 1.0 - installationSpans[cmdName].Succeeded() - installationSpans[cmdName].End() - - progress.WriteLine(output.Styledf(output.StyleSuccess, "%s installed", cmdName)) - - progress.SetValue(0, done) - progress.SetLabelAndRecalc(0, fmt.Sprintf("%d/%d commands installed", int(done), int(total))) - - // Everything installed! - if len(cmdNames) == 0 { - progress.Complete() - - duration := time.Since(installationStart) - - std.Out.Write("") - if c.verbose { - std.Out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Took %s. Booting up the system!", duration)) - } else { - std.Out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) - } - std.Out.Write("") - - close(c.okayToStart) - return nil - } - - case failure := <-c.failures: - progress.Destroy() - installationSpans[failure.cmdName].RecordError("failed", failure.err) - installationSpans[failure.cmdName].End() - - // Something went wrong with an installation, no need to wait for the others - printCmdError(std.Out.Output, failure.cmdName, failure.err) - return failure - case <-ticker.C: - names := []string{} - for name := range cmdNames { - names = append(names, name) - } - - idx := messageCount - if idx > len(waitingMessages)-1 { - idx = len(waitingMessages) - 1 - } - msg := waitingMessages[idx] - - emoji := output.EmojiHourglass - if idx > 3 { - emoji = output.EmojiShrug - } - - progress.WriteLine(output.Linef(emoji, output.StyleBold, msg, strings.Join(names, ", "))) - messageCount += 1 - } + return oldHash != newHash, nil } + // If there is no installer, then we always restart + return true, nil } // failedRun is returned by run when a command failed to run and run exits @@ -433,17 +202,6 @@ func (e installErr) Error() string { return fmt.Sprintf("install of %s failed: %s", e.cmdName, e.output) } -// reinstallErr is used internally by runWatch to print a message when a -// command failed to reinstall. -type reinstallErr struct { - cmdName string - output string -} - -func (e reinstallErr) Error() string { - return fmt.Sprintf("reinstalling %s failed: %s", e.cmdName, e.output) -} - // runErr is used internally by runWatch to print a message when a // command failed to reinstall. type runErr struct { @@ -473,9 +231,6 @@ func printCmdError(out *output.Output, cmdName string, err error) { } } cmdOut = e.output - case reinstallErr: - message = "Failed to rebuild " + cmdName - cmdOut = e.output case runErr: message = "Failed to run " + cmdName cmdOut = fmt.Sprintf("Exit code: %d\n\n", e.exitCode) @@ -641,131 +396,16 @@ func md5HashFile(filename string) (string, error) { return string(h.Sum(nil)), nil } -// -// - -type changeMonitor struct { - subscriptions []subscription -} - -type subscription struct { - cmd Command - ch chan struct{} -} - -func (m *changeMonitor) run(paths <-chan string) { - for path := range paths { - for _, sub := range m.subscriptions { - m.notify(sub, path) - } - } -} - -func (m *changeMonitor) notify(sub subscription, path string) { - found := false - for _, prefix := range sub.cmd.Watch { - if strings.HasPrefix(path, prefix) { - found = true - } - } - if !found { - return - } - - select { - case sub.ch <- struct{}{}: - default: - } -} - -func (m *changeMonitor) register(cmd Command) <-chan struct{} { - ch := make(chan struct{}) - m.subscriptions = append(m.subscriptions, subscription{cmd, ch}) - return ch -} - -// -// - -var watchIgnorePatterns = []*regexp.Regexp{ - regexp.MustCompile(`_test\.go$`), - regexp.MustCompile(`^.bin/`), - regexp.MustCompile(`^.git/`), - regexp.MustCompile(`^dev/`), - regexp.MustCompile(`^node_modules/`), -} - -func watch() (<-chan string, error) { - repoRoot, err := root.RepositoryRoot() - if err != nil { - return nil, err - } - - paths := make(chan string) - events := make(chan notify.EventInfo, 1) - - if err := notify.Watch(repoRoot+"/...", events, notify.All); err != nil { - return nil, err - } - - go func() { - defer close(events) - defer notify.Stop(events) - - outer: - for event := range events { - path := strings.TrimPrefix(strings.TrimPrefix(event.Path(), repoRoot), "/") - - for _, pattern := range watchIgnorePatterns { - if pattern.MatchString(path) { - continue outer - } - } - - paths <- path - } - }() - - return paths, nil -} - -func Test(ctx context.Context, cmd Command, args []string, parentEnv map[string]string) error { +func Test(ctx context.Context, cmd ConfigCommand, parentEnv map[string]string) error { repoRoot, err := root.RepositoryRoot() if err != nil { return err } - std.Out.WriteLine(output.Styledf(output.StylePending, "Starting testsuite %q.", cmd.Name)) - if len(args) != 0 { - std.Out.WriteLine(output.Styledf(output.StylePending, "\tAdditional arguments: %s", args)) - } - commandCtx, cancel := context.WithCancel(ctx) - defer cancel() - - cmdArgs := []string{cmd.Cmd} - if len(args) != 0 { - cmdArgs = append(cmdArgs, args...) - } else { - cmdArgs = append(cmdArgs, cmd.DefaultArgs) - } - - secretsEnv, err := getSecrets(ctx, cmd.Name, cmd.ExternalSecrets) + std.Out.WriteLine(output.Styledf(output.StylePending, "Starting testsuite %q.", cmd.GetName())) + sc, err := startConfigCmd(ctx, cmd, repoRoot, parentEnv) if err != nil { - std.Out.WriteLine(output.Styledf(output.StyleWarning, "[%s] %s %s", - cmd.Name, output.EmojiFailure, err.Error())) + printCmdError(std.Out.Output, cmd.GetName(), err) } - - if cmd.Preamble != "" { - std.Out.WriteLine(output.Styledf(output.StyleOrange, "[%s] %s %s", cmd.Name, output.EmojiInfo, cmd.Preamble)) - } - - c := exec.CommandContext(commandCtx, "bash", "-c", strings.Join(cmdArgs, " ")) - c.Dir = repoRoot - c.Env = makeEnv(parentEnv, secretsEnv, cmd.Env) - c.Stdout = os.Stdout - c.Stderr = os.Stderr - - std.Out.WriteLine(output.Styledf(output.StylePending, "Running %s in %q...", c, repoRoot)) - - return c.Run() + return sc.Wait() } diff --git a/dev/sg/internal/run/run_bazel.go b/dev/sg/internal/run/run_bazel.go deleted file mode 100644 index 76d226ba4588..000000000000 --- a/dev/sg/internal/run/run_bazel.go +++ /dev/null @@ -1,73 +0,0 @@ -package run - -import ( - "context" - "fmt" - "os/exec" - "strings" - - "github.com/sourcegraph/conc/pool" - - "github.com/sourcegraph/sourcegraph/dev/sg/root" -) - -func outputPath() ([]byte, error) { - // Get the output directory from Bazel, which varies depending on which OS - // we're running against. - cmd := exec.Command("bazel", "info", "output_path") - return cmd.Output() -} - -// binLocation returns the path on disk where Bazel is putting the binary -// associated with a given target. -func binLocation(target string) (string, error) { - baseOutput, err := outputPath() - if err != nil { - return "", err - } - // Trim "bazel-out" because the next bazel query will include it. - outputPath := strings.TrimSuffix(strings.TrimSpace(string(baseOutput)), "bazel-out") - - // Get the binary from the specific target. - cmd := exec.Command("bazel", "cquery", target, "--output=files") - baseOutput, err = cmd.Output() - if err != nil { - return "", err - } - binPath := strings.TrimSpace(string(baseOutput)) - - return fmt.Sprintf("%s%s", outputPath, binPath), nil -} - -func BazelCommands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...BazelCommand) error { - if len(cmds) == 0 { - // no Bazel commands so we return - return nil - } - - repoRoot, err := root.RepositoryRoot() - if err != nil { - return err - } - - var targets []string - for _, cmd := range cmds { - targets = append(targets, cmd.Target) - } - - ibazel := newIBazel(repoRoot, targets...) - - p := pool.New().WithContext(ctx).WithCancelOnError() - p.Go(func(ctx context.Context) error { - return ibazel.Start(ctx, repoRoot) - }) - - for _, bc := range cmds { - bc := bc - p.Go(func(ctx context.Context) error { - return bc.Start(ctx, repoRoot, parentEnv) - }) - } - - return p.Wait() -} diff --git a/dev/sg/sg_run.go b/dev/sg/sg_run.go index a30fe02916e5..0c2343d178b5 100644 --- a/dev/sg/sg_run.go +++ b/dev/sg/sg_run.go @@ -92,48 +92,36 @@ func runExec(ctx *cli.Context) error { return flag.ErrHelp } - var cmds []run.Command - var bcmds []run.BazelCommand + cmds := make([]run.ConfigCommand, 0, len(args)) for _, arg := range args { - if bazelCmd, okB := config.BazelCommands[arg]; okB && !legacy { - bcmds = append(bcmds, bazelCmd) - } else { - cmd, okC := config.Commands[arg] - if !okC && !okB { - std.Out.WriteLine(output.Styledf(output.StyleWarning, "ERROR: command %q not found :(", arg)) - return flag.ErrHelp - } + if bazelCmd, ok := config.BazelCommands[arg]; ok && !legacy { + cmds = append(cmds, bazelCmd) + } else if cmd, ok := config.Commands[arg]; ok { cmds = append(cmds, cmd) + } else { + std.Out.WriteLine(output.Styledf(output.StyleWarning, "ERROR: command %q not found :(", arg)) + return flag.ErrHelp } } if ctx.Bool("describe") { - // TODO Bazel commands for _, cmd := range cmds { out, err := yaml.Marshal(cmd) if err != nil { return err } - std.Out.WriteMarkdown(fmt.Sprintf("# %s\n\n```yaml\n%s\n```\n\n", cmd.Name, string(out))) + if err = std.Out.WriteMarkdown(fmt.Sprintf("# %s\n\n```yaml\n%s\n```\n\n", cmd.GetName(), string(out))); err != nil { + return err + } } return nil } - if !legacy { - // First we build everything once, to ensure all binaries are present. - if err := run.BazelBuild(ctx.Context, bcmds...); err != nil { - return err - } - } - p := pool.New().WithContext(ctx.Context).WithCancelOnError() p.Go(func(ctx context.Context) error { return run.Commands(ctx, config.Env, verbose, cmds...) }) - p.Go(func(ctx context.Context) error { - return run.BazelCommands(ctx, config.Env, verbose, bcmds...) - }) return p.Wait() } diff --git a/dev/sg/sg_start.go b/dev/sg/sg_start.go index f9faeeecd4ab..c50847fb0749 100644 --- a/dev/sg/sg_start.go +++ b/dev/sg/sg_start.go @@ -8,10 +8,8 @@ import ( "path/filepath" "sort" "strings" - "sync" "time" - "github.com/sourcegraph/conc/pool" sgrun "github.com/sourcegraph/run" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" @@ -178,8 +176,6 @@ func constructStartCmdLongHelp() string { return out.String() } -var sgOnce sync.Once - func startExec(ctx *cli.Context) error { config, err := getConfig() if err != nil { @@ -305,6 +301,66 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C return err } + repoRoot, err := root.RepositoryRoot() + if err != nil { + return err + } + + cmds, err := getCommands(set.Commands, set, conf.Commands) + if err != nil { + return err + } + + bcmds, err := getCommands(set.BazelCommands, set, conf.BazelCommands) + if err != nil { + return err + } + + if len(cmds) == 0 && len(bcmds) == 0 { + std.Out.WriteLine(output.Styled(output.StyleWarning, "WARNING: no commands to run")) + return nil + } + + levelOverrides := logLevelOverrides() + for _, cmd := range cmds { + enrichWithLogLevels(&cmd, levelOverrides) + } + + env := conf.Env + for k, v := range set.Env { + env[k] = v + } + + installers := make([]run.Installer, 0, len(cmds)+1) + for _, cmd := range cmds { + installers = append(installers, cmd) + } + + if len(bcmds) > 0 { + ibazel, err := run.NewIBazel(bcmds, repoRoot) + if err != nil { + return err + } + defer ibazel.Stop() + installers = append(installers, ibazel) + } + + if err := run.Install(ctx, env, verbose, installers...); err != nil { + return err + } + + configCmds := make([]run.ConfigCommand, 0, len(bcmds)+len(cmds)) + for _, cmd := range bcmds { + configCmds = append(configCmds, cmd) + } + + for _, cmd := range cmds { + configCmds = append(configCmds, cmd) + } + return run.Commands(ctx, env, verbose, configCmds...) +} + +func getCommands[T run.ConfigCommand](commands []string, set *sgconf.Commandset, conf map[string]T) ([]T, error) { exceptList := exceptServices.Value() exceptSet := make(map[string]interface{}, len(exceptList)) for _, svc := range exceptList { @@ -317,15 +373,15 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C onlySet[svc] = struct{}{} } - cmds := make([]run.Command, 0, len(set.Commands)) - for _, name := range set.Commands { - cmd, ok := conf.Commands[name] + cmds := make([]T, 0, len(commands)) + for _, name := range commands { + cmd, ok := conf[name] if !ok { - return errors.Errorf("command %q not found in commandset %q", name, set.Name) + return nil, errors.Errorf("command %q not found in commandset %q", name, set.Name) } if _, excluded := exceptSet[name]; excluded { - std.Out.WriteLine(output.Styledf(output.StylePending, "Skipping command %s since it's in --except.", cmd.Name)) + std.Out.WriteLine(output.Styledf(output.StylePending, "Skipping command %s since it's in --except.", cmd.GetName())) continue } @@ -336,50 +392,12 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C if _, inSet := onlySet[name]; inSet { cmds = append(cmds, cmd) } else { - std.Out.WriteLine(output.Styledf(output.StylePending, "Skipping command %s since it's not included in --only.", cmd.Name)) + std.Out.WriteLine(output.Styledf(output.StylePending, "Skipping command %s since it's not included in --only.", cmd.GetName())) } } } - - bcmds := make([]run.BazelCommand, 0, len(set.BazelCommands)) - for _, name := range set.BazelCommands { - bcmd, ok := conf.BazelCommands[name] - if !ok { - return errors.Errorf("command %q not found in commandset %q", name, set.Name) - } - - bcmds = append(bcmds, bcmd) - } - if len(cmds) == 0 && len(bcmds) == 0 { - std.Out.WriteLine(output.Styled(output.StyleWarning, "WARNING: no commands to run")) - return nil - } - - levelOverrides := logLevelOverrides() - for _, cmd := range cmds { - enrichWithLogLevels(&cmd, levelOverrides) - } - - env := conf.Env - for k, v := range set.Env { - env[k] = v - } - - // First we build everything once, to ensure all binaries are present. - if err := run.BazelBuild(ctx, bcmds...); err != nil { - return err - } - - p := pool.New().WithContext(ctx).WithCancelOnError() - p.Go(func(ctx context.Context) error { - return run.Commands(ctx, env, verbose, cmds...) - }) - p.Go(func(ctx context.Context) error { - return run.BazelCommands(ctx, env, verbose, bcmds...) - }) - - return p.Wait() + return cmds, nil } // logLevelOverrides builds a map of commands -> log level that should be overridden in the environment. diff --git a/dev/sg/sg_tests.go b/dev/sg/sg_tests.go index 1f1946de5b68..cb1a4368c27c 100644 --- a/dev/sg/sg_tests.go +++ b/dev/sg/sg_tests.go @@ -1,8 +1,10 @@ package main import ( + "context" "flag" "fmt" + "os/exec" "sort" "strings" @@ -71,7 +73,7 @@ func testExec(ctx *cli.Context) error { return flag.ErrHelp } - return run.Test(ctx.Context, cmd, args[1:], config.Env) + return run.Test(ctx.Context, newSGTestCommand(cmd, args[1:]), config.Env) } func constructTestCmdLongHelp() string { @@ -102,3 +104,28 @@ func constructTestCmdLongHelp() string { return out.String() } + +type sgTestCommand struct { + run.Command + args []string +} + +// Ovrrides the GetExec method with a custom implementation to construct the command +// using CLI-passed arguments +func (test sgTestCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { + cmdArgs := []string{test.Command.Cmd} + if len(test.args) != 0 { + cmdArgs = append(cmdArgs, test.args...) + } else { + cmdArgs = append(cmdArgs, test.Command.DefaultArgs) + } + + return exec.CommandContext(ctx, "bash", "-c", strings.Join(cmdArgs, " ")), nil +} + +func newSGTestCommand(cmd run.Command, args []string) sgTestCommand { + return sgTestCommand{ + Command: cmd, + args: args, + } +} diff --git a/go.mod b/go.mod index 164f93e5348c..e5fe39b30ee7 100644 --- a/go.mod +++ b/go.mod @@ -354,6 +354,7 @@ require ( github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.11 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/onsi/gomega v1.27.8 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect @@ -387,6 +388,7 @@ require ( go.uber.org/goleak v1.3.0 // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) require ( diff --git a/go.sum b/go.sum index 65d906035b75..0e8b4aaa4b2c 100644 --- a/go.sum +++ b/go.sum @@ -1405,6 +1405,8 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= diff --git a/internal/codeintel/uploads/internal/store/processing.go b/internal/codeintel/uploads/internal/store/processing.go index 0ad7fc4138da..f8b1fe237948 100644 --- a/internal/codeintel/uploads/internal/store/processing.go +++ b/internal/codeintel/uploads/internal/store/processing.go @@ -15,6 +15,8 @@ import ( dbworkerstore "github.com/sourcegraph/sourcegraph/internal/workerutil/dbworker/store" ) +// adds a comment + // InsertUpload inserts a new upload and returns its identifier. func (s *store) InsertUpload(ctx context.Context, upload shared.Upload) (id int, err error) { ctx, _, endObservation := s.operations.insertUpload.With(ctx, &err, observation.Args{}) diff --git a/lib/errors/filter.go b/lib/errors/filter.go index 6a6b4b26363a..a3adb2e90a3a 100644 --- a/lib/errors/filter.go +++ b/lib/errors/filter.go @@ -12,6 +12,7 @@ func Ignore(err error, pred ErrorPredicate) error { // If the error (or any wrapped error) is a multierror, // filter its children. var multi *multiError + println("CHANGE") if As(err, &multi) { filtered := multi.errs[:0] for _, childErr := range multi.errs { diff --git a/sg.config.yaml b/sg.config.yaml index 62dba78f88ed..525d090424e9 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -406,6 +406,8 @@ commands: web: description: Enterprise version of the web app cmd: pnpm --filter @sourcegraph/web dev + watch: + - client/web/dev install: | pnpm install pnpm run generate @@ -972,6 +974,8 @@ bazelCommands: target: //cmd/blobstore:blobstore searcher: target: //cmd/searcher + docsite: + target: //doc:serve syntax-highlighter: target: //docker-images/syntax-highlighter:syntect_server ignoreStdout: true @@ -987,6 +991,7 @@ bazelCommands: QUIET: 'true' frontend: description: Enterprise frontend + continueWatchOnExit: true target: //cmd/frontend precmd: | export SOURCEGRAPH_LICENSE_GENERATION_KEY=$(cat ../dev-private/enterprise/dev/test-license-generation-key.pem) @@ -1017,7 +1022,7 @@ bazelCommands: USE_ROCKSKIP: 'false' gitserver-template: &gitserver_bazel_template target: //cmd/gitserver - env: &gitserverenv + env: HOSTNAME: 127.0.0.1:3178 # This is only here to stay backwards-compatible with people's custom # `sg.config.overwrite.yaml` files @@ -1074,7 +1079,8 @@ commandsets: - bazelisk - ibazel bazelCommands: - - blobstore + # - blobstore + - docsite - frontend - worker - repo-updater @@ -1082,7 +1088,7 @@ commandsets: - gitserver-1 - searcher - symbols - - syntax-highlighter + # - syntax-highlighter commands: - web # TODO https://github.com/sourcegraph/devx-support/issues/537 @@ -1092,6 +1098,12 @@ commandsets: - zoekt-web-0 - zoekt-web-1 - caddy + simple: + bazelCommands: + - worker + commands: + - web + - docsite # If you modify this command set, please consider also updating the dotcom runset. enterprise: &enterprise_set requiresDevPrivate: true From b722598a2cbd643e0cfe4913f4d51d5658d578b8 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 18 Jan 2024 20:58:04 -0800 Subject: [PATCH 08/17] cleanup --- cmd/frontend/main.go | 2 - cmd/worker/BUILD.bazel | 2 +- dev/bazel_configure_accept_changes.sh | 3 + dev/sg/internal/run/BUILD.bazel | 2 +- dev/sg/internal/run/bazel_command.go | 1 - dev/sg/internal/run/command.go | 2 +- dev/sg/internal/run/installer.go | 82 ++++++++++++-- dev/sg/internal/run/run.go | 107 ++++-------------- ...{config_command.go => sgconfig_command.go} | 7 +- dev/sg/sg_run.go | 2 +- dev/sg/sg_start.go | 4 +- .../uploads/internal/store/processing.go | 2 - lib/errors/filter.go | 1 - sg.config.yaml | 5 - 14 files changed, 111 insertions(+), 111 deletions(-) rename dev/sg/internal/run/{config_command.go => sgconfig_command.go} (92%) diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index 5f83c6222382..a744d44d7cd1 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -16,8 +16,6 @@ import ( func main() { sanitycheck.Pass() - println("SUP") - // os.Exit(10) if os.Getenv("WEB_BUILDER_DEV_SERVER") == "1" { assets.UseDevAssetsProvider() } diff --git a/cmd/worker/BUILD.bazel b/cmd/worker/BUILD.bazel index 11a7e2ceffb1..ed73e3e183a2 100644 --- a/cmd/worker/BUILD.bazel +++ b/cmd/worker/BUILD.bazel @@ -1,5 +1,5 @@ -load("@container_structure_test//:defs.bzl", "container_structure_test") load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") +load("@container_structure_test//:defs.bzl", "container_structure_test") load("@rules_oci//oci:defs.bzl", "oci_image", "oci_push", "oci_tarball") load("@rules_pkg//:pkg.bzl", "pkg_tar") load("//dev:oci_defs.bzl", "image_repository") diff --git a/dev/bazel_configure_accept_changes.sh b/dev/bazel_configure_accept_changes.sh index b6eb3fbf4cd3..b78f2d29032a 100755 --- a/dev/bazel_configure_accept_changes.sh +++ b/dev/bazel_configure_accept_changes.sh @@ -1,6 +1,9 @@ #! /bin/bash # Run bazel configure and if the error code is 110, exit with error code 0 +# This is because 110 means that configuration files were successfully +# Can be used by processes which want to run configuration as an auto-fix +# and expect a 0 exit code bazel configure exit_code=$? diff --git a/dev/sg/internal/run/BUILD.bazel b/dev/sg/internal/run/BUILD.bazel index 60940e789220..aeeba08d4b76 100644 --- a/dev/sg/internal/run/BUILD.bazel +++ b/dev/sg/internal/run/BUILD.bazel @@ -6,7 +6,6 @@ go_library( srcs = [ "bazel_command.go", "command.go", - "config_command.go", "helpers.go", "ibazel.go", "installer.go", @@ -14,6 +13,7 @@ go_library( "pid.go", "prefix_suffix_saver.go", "run.go", + "sgconfig_command.go", ], importpath = "github.com/sourcegraph/sourcegraph/dev/sg/internal/run", visibility = ["//dev/sg:__subpackages__"], diff --git a/dev/sg/internal/run/bazel_command.go b/dev/sg/internal/run/bazel_command.go index 924fc6ad1c17..ab8514be90da 100644 --- a/dev/sg/internal/run/bazel_command.go +++ b/dev/sg/internal/run/bazel_command.go @@ -97,7 +97,6 @@ func (bc BazelCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { if err != nil { return nil, err } - println("Binary location: " + binLocation + "\n") return exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, binLocation)), nil } diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 060711d3b322..027706f95fa5 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -306,7 +306,7 @@ func OpenUnixSocket() error { return err } -func startConfigCmd(ctx context.Context, cmd ConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { +func startSgCmd(ctx context.Context, cmd SGConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { exec, err := cmd.GetExec(ctx) if err != nil { return nil, err diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go index 951c0b2c331b..82d3b6c65d7e 100644 --- a/dev/sg/internal/run/installer.go +++ b/dev/sg/internal/run/installer.go @@ -3,12 +3,18 @@ package run import ( "context" "fmt" + "os" + "path/filepath" + "runtime" "strings" "time" "github.com/sourcegraph/sourcegraph/dev/sg/internal/analytics" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" "github.com/sourcegraph/sourcegraph/dev/sg/interrupt" + "github.com/sourcegraph/sourcegraph/dev/sg/root" + "github.com/sourcegraph/sourcegraph/internal/download" + "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" ) @@ -19,7 +25,7 @@ type Installer interface { type InstallManager struct { // Constructor commands - out *std.Output + *std.Output cmds map[string]struct{} env map[string]string verbose bool @@ -49,7 +55,7 @@ func Install(ctx context.Context, parentEnv map[string]string, verbose bool, cmd func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, verbose bool) *InstallManager { total := len(cmds) return &InstallManager{ - out: out, + Output: out, cmds: SliceToHashSet(cmds, func(c Installer) string { return c.GetName() }), verbose: verbose, env: env, @@ -63,9 +69,9 @@ func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, // starts all progress bars and counters but does not start installation func (installer *InstallManager) start(ctx context.Context) { - installer.out.Write("") - installer.out.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", installer.total)) - installer.out.Write("") + installer.Write("") + installer.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", installer.total)) + installer.Write("") installer.progress = std.Out.Progress([]output.ProgressBar{ {Label: fmt.Sprintf("Installing %d commands", installer.total), Max: float64(installer.total)}, @@ -140,19 +146,19 @@ func (installer *InstallManager) handleInstalled(name string) { func (installer *InstallManager) complete() { installer.progress.Complete() - installer.out.Write("") + installer.Write("") if installer.verbose { - installer.out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Took %s. Booting up the system!", installer.stats.duration())) + installer.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Took %s. Booting up the system!", installer.stats.duration())) } else { - installer.out.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) + installer.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) } - installer.out.Write("") + installer.Write("") } func (installer *InstallManager) handleFailure(name string, err error) { installer.progress.Destroy() installer.stats.handleFailure(name, err) - printCmdError(installer.out.Output, name, err) + printCmdError(installer.Output.Output, name, err) } func (installer *InstallManager) handleWaiting() { @@ -232,6 +238,62 @@ func SliceToHashSet[R any, T comparable](slice []R, extract func(R) T) HashSet[T return set } +type installFunc func(context.Context, map[string]string) error + +var installFuncs = map[string]installFunc{ + "installCaddy": func(ctx context.Context, env map[string]string) error { + version := env["CADDY_VERSION"] + if version == "" { + return errors.New("could not find CADDY_VERSION in env") + } + + root, err := root.RepositoryRoot() + if err != nil { + return err + } + + var os string + switch runtime.GOOS { + case "linux": + os = "linux" + case "darwin": + os = "mac" + } + + archiveName := fmt.Sprintf("caddy_%s_%s_%s", version, os, runtime.GOARCH) + url := fmt.Sprintf("https://github.com/caddyserver/caddy/releases/download/v%s/%s.tar.gz", version, archiveName) + + target := filepath.Join(root, fmt.Sprintf(".bin/caddy_%s", version)) + + return download.ArchivedExecutable(ctx, url, target, "caddy") + }, + "installJaeger": func(ctx context.Context, env map[string]string) error { + version := env["JAEGER_VERSION"] + + // Make sure the data folder exists. + disk := env["JAEGER_DISK"] + if err := os.MkdirAll(disk, 0755); err != nil { + return err + } + + if version == "" { + return errors.New("could not find JAEGER_VERSION in env") + } + + root, err := root.RepositoryRoot() + if err != nil { + return err + } + + archiveName := fmt.Sprintf("jaeger-%s-%s-%s", version, runtime.GOOS, runtime.GOARCH) + url := fmt.Sprintf("https://github.com/jaegertracing/jaeger/releases/download/v%s/%s.tar.gz", version, archiveName) + + target := filepath.Join(root, fmt.Sprintf(".bin/jaeger-all-in-one-%s", version)) + + return download.ArchivedExecutable(ctx, url, target, fmt.Sprintf("%s/jaeger-all-in-one", archiveName)) + }, +} + var waitingMessages = []string{ "Still waiting for %s to finish installing...", "Yup, still waiting for %s to finish installing...", diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index 79e0044b222f..a3d868d95988 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -7,21 +7,19 @@ import ( "io" "os" "path/filepath" - "runtime" "strings" "github.com/sourcegraph/conc/pool" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" "github.com/sourcegraph/sourcegraph/dev/sg/root" - "github.com/sourcegraph/sourcegraph/internal/download" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" ) -func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...ConfigCommand) (err error) { +func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...SGConfigCommand) (err error) { if len(cmds) == 0 { - // no Bazel commands so we return + // Exit early if there are no commands to run. return nil } std.Out.WriteLine(output.Styled(output.StylePending, fmt.Sprintf("Starting %d cmds", len(cmds)))) @@ -31,6 +29,14 @@ func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cm return err } + // binaries get installed to /.bin. If the binary is installed with go build, then go + // will create .bin directory. Some binaries (like docsite) get downloaded instead of built and therefore + // need the directory to exist before hand. + binDir := filepath.Join(repoRoot, ".bin") + if err := os.Mkdir(binDir, 0755); err != nil && !os.IsExist(err) { + return err + } + runner := cmdRunner{ std.Out, cmds, @@ -44,7 +50,7 @@ func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cm func (runner *cmdRunner) run(ctx context.Context) error { p := pool.New().WithContext(ctx).WithCancelOnError() - // Start each Bazel command concurrently + // Start each command concurrently for _, cmd := range runner.cmds { cmd := cmd p.Go(func(ctx context.Context) error { @@ -53,7 +59,6 @@ func (runner *cmdRunner) run(ctx context.Context) error { // Start watching the commands dependencies wantRestart, err := cmd.StartWatch(ctx) if err != nil { - runner.Write("Failed to watch " + cmd.GetName()) runner.printError(cmd, err) return err } @@ -61,7 +66,6 @@ func (runner *cmdRunner) run(ctx context.Context) error { // start up the binary sc, err := runner.start(ctx, cmd) if err != nil { - runner.Write("Failed to start " + cmd.GetName()) runner.printError(cmd, err) return errors.Wrapf(err, "failed to start command %q", cmd.GetName()) } @@ -73,14 +77,12 @@ func (runner *cmdRunner) run(ctx context.Context) error { select { // Handle context cancelled case <-ctx.Done(): - runner.debug("context error" + cmd.GetName()) return ctx.Err() // Handle process exit case err := <-sc.ErrorChannel(): - // Exited on its own or errored + // If the process failed, we exit immedieatly if err != nil { - runner.debug("Error channel " + cmd.GetName()) return err } runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.GetName(), output.StyleReset)) @@ -96,42 +98,38 @@ func (runner *cmdRunner) run(ctx context.Context) error { runner.WriteLine(output.Styledf(output.StylePending, "Change detected. Reloading %s...", cmd.GetName())) shouldRestart, err := runner.reinstall(ctx, cmd) if err != nil { - runner.debug("reinstall failure: %s", cmd.GetName()) runner.printError(cmd, err) return err } if shouldRestart { - std.Out.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", cmd.GetName())) + runner.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", cmd.GetName())) sc.cancel() sc, err = runner.start(ctx, cmd) - defer sc.cancel() if err != nil { - runner.debug("restart failure " + cmd.GetName()) return err } + defer sc.cancel() } else { - std.Out.WriteLine(output.Styledf(output.StylePending, "Binary for %s did not change. Not restarting.", cmd.GetName())) + runner.WriteLine(output.Styledf(output.StylePending, "Binary for %s did not change. Not restarting.", cmd.GetName())) } } } }) } - err := p.Wait() - runner.Write("Completed all commands") - return err + return p.Wait() } type cmdRunner struct { *std.Output - cmds []ConfigCommand + cmds []SGConfigCommand repositoryRoot string parentEnv map[string]string verbose bool } -func (runner *cmdRunner) printError(cmd ConfigCommand, err error) { +func (runner *cmdRunner) printError(cmd SGConfigCommand, err error) { printCmdError(runner.Output.Output, cmd.GetName(), err) } @@ -142,17 +140,16 @@ func (runner *cmdRunner) debug(msg string, args ...any) { } } -func (runner *cmdRunner) start(ctx context.Context, cmd ConfigCommand) (*startedCmd, error) { - return startConfigCmd(ctx, cmd, runner.repositoryRoot, runner.parentEnv) +func (runner *cmdRunner) start(ctx context.Context, cmd SGConfigCommand) (*startedCmd, error) { + return startSgCmd(ctx, cmd, runner.repositoryRoot, runner.parentEnv) } -func (runner *cmdRunner) reinstall(ctx context.Context, cmd ConfigCommand) (bool, error) { +func (runner *cmdRunner) reinstall(ctx context.Context, cmd SGConfigCommand) (bool, error) { if installer, ok := cmd.(Installer); ok { bin, err := cmd.GetBinaryLocation() if err != nil { - noBinary := noBinaryError{} // If the command doesn't have a CheckBinary, we just ignore it - if errors.As(err, &noBinary) { + if errors.Is(err, noBinaryError{}) { return false, nil } else { return false, err @@ -269,62 +266,6 @@ func printCmdError(out *output.Output, cmdName string, err error) { } } -type installFunc func(context.Context, map[string]string) error - -var installFuncs = map[string]installFunc{ - "installCaddy": func(ctx context.Context, env map[string]string) error { - version := env["CADDY_VERSION"] - if version == "" { - return errors.New("could not find CADDY_VERSION in env") - } - - root, err := root.RepositoryRoot() - if err != nil { - return err - } - - var os string - switch runtime.GOOS { - case "linux": - os = "linux" - case "darwin": - os = "mac" - } - - archiveName := fmt.Sprintf("caddy_%s_%s_%s", version, os, runtime.GOARCH) - url := fmt.Sprintf("https://github.com/caddyserver/caddy/releases/download/v%s/%s.tar.gz", version, archiveName) - - target := filepath.Join(root, fmt.Sprintf(".bin/caddy_%s", version)) - - return download.ArchivedExecutable(ctx, url, target, "caddy") - }, - "installJaeger": func(ctx context.Context, env map[string]string) error { - version := env["JAEGER_VERSION"] - - // Make sure the data folder exists. - disk := env["JAEGER_DISK"] - if err := os.MkdirAll(disk, 0755); err != nil { - return err - } - - if version == "" { - return errors.New("could not find JAEGER_VERSION in env") - } - - root, err := root.RepositoryRoot() - if err != nil { - return err - } - - archiveName := fmt.Sprintf("jaeger-%s-%s-%s", version, runtime.GOOS, runtime.GOARCH) - url := fmt.Sprintf("https://github.com/jaegertracing/jaeger/releases/download/v%s/%s.tar.gz", version, archiveName) - - target := filepath.Join(root, fmt.Sprintf(".bin/jaeger-all-in-one-%s", version)) - - return download.ArchivedExecutable(ctx, url, target, fmt.Sprintf("%s/jaeger-all-in-one", archiveName)) - }, -} - // makeEnv merges environments starting from the left, meaning the first environment will be overriden by the second one, skipping // any key that has been explicitly defined in the current environment of this process. This enables users to manually overrides // environment variables explictly, i.e FOO=1 sg start will have FOO=1 set even if a command or commandset sets FOO. @@ -396,14 +337,14 @@ func md5HashFile(filename string) (string, error) { return string(h.Sum(nil)), nil } -func Test(ctx context.Context, cmd ConfigCommand, parentEnv map[string]string) error { +func Test(ctx context.Context, cmd SGConfigCommand, parentEnv map[string]string) error { repoRoot, err := root.RepositoryRoot() if err != nil { return err } std.Out.WriteLine(output.Styledf(output.StylePending, "Starting testsuite %q.", cmd.GetName())) - sc, err := startConfigCmd(ctx, cmd, repoRoot, parentEnv) + sc, err := startSgCmd(ctx, cmd, repoRoot, parentEnv) if err != nil { printCmdError(std.Out.Output, cmd.GetName(), err) } diff --git a/dev/sg/internal/run/config_command.go b/dev/sg/internal/run/sgconfig_command.go similarity index 92% rename from dev/sg/internal/run/config_command.go rename to dev/sg/internal/run/sgconfig_command.go index be9a2cc8660d..ddfda63ae0ee 100644 --- a/dev/sg/internal/run/config_command.go +++ b/dev/sg/internal/run/sgconfig_command.go @@ -10,7 +10,7 @@ import ( "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" ) -type ConfigCommand interface { +type SGConfigCommand interface { // Getters for common fields GetName() string GetContinueWatchOnExit() bool @@ -78,3 +78,8 @@ func (e noBinaryError) Wrap(err error) error { e.err = err return e } + +func (e noBinaryError) Is(target error) bool { + _, ok := target.(noBinaryError) + return ok +} diff --git a/dev/sg/sg_run.go b/dev/sg/sg_run.go index 0c2343d178b5..a6d52df6c5ab 100644 --- a/dev/sg/sg_run.go +++ b/dev/sg/sg_run.go @@ -92,7 +92,7 @@ func runExec(ctx *cli.Context) error { return flag.ErrHelp } - cmds := make([]run.ConfigCommand, 0, len(args)) + cmds := make([]run.SGConfigCommand, 0, len(args)) for _, arg := range args { if bazelCmd, ok := config.BazelCommands[arg]; ok && !legacy { cmds = append(cmds, bazelCmd) diff --git a/dev/sg/sg_start.go b/dev/sg/sg_start.go index c50847fb0749..8a69b5e0ecb3 100644 --- a/dev/sg/sg_start.go +++ b/dev/sg/sg_start.go @@ -349,7 +349,7 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C return err } - configCmds := make([]run.ConfigCommand, 0, len(bcmds)+len(cmds)) + configCmds := make([]run.SGConfigCommand, 0, len(bcmds)+len(cmds)) for _, cmd := range bcmds { configCmds = append(configCmds, cmd) } @@ -360,7 +360,7 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C return run.Commands(ctx, env, verbose, configCmds...) } -func getCommands[T run.ConfigCommand](commands []string, set *sgconf.Commandset, conf map[string]T) ([]T, error) { +func getCommands[T run.SGConfigCommand](commands []string, set *sgconf.Commandset, conf map[string]T) ([]T, error) { exceptList := exceptServices.Value() exceptSet := make(map[string]interface{}, len(exceptList)) for _, svc := range exceptList { diff --git a/internal/codeintel/uploads/internal/store/processing.go b/internal/codeintel/uploads/internal/store/processing.go index f8b1fe237948..0ad7fc4138da 100644 --- a/internal/codeintel/uploads/internal/store/processing.go +++ b/internal/codeintel/uploads/internal/store/processing.go @@ -15,8 +15,6 @@ import ( dbworkerstore "github.com/sourcegraph/sourcegraph/internal/workerutil/dbworker/store" ) -// adds a comment - // InsertUpload inserts a new upload and returns its identifier. func (s *store) InsertUpload(ctx context.Context, upload shared.Upload) (id int, err error) { ctx, _, endObservation := s.operations.insertUpload.With(ctx, &err, observation.Args{}) diff --git a/lib/errors/filter.go b/lib/errors/filter.go index a3adb2e90a3a..6a6b4b26363a 100644 --- a/lib/errors/filter.go +++ b/lib/errors/filter.go @@ -12,7 +12,6 @@ func Ignore(err error, pred ErrorPredicate) error { // If the error (or any wrapped error) is a multierror, // filter its children. var multi *multiError - println("CHANGE") if As(err, &multi) { filtered := multi.errs[:0] for _, childErr := range multi.errs { diff --git a/sg.config.yaml b/sg.config.yaml index 525d090424e9..ea831bddff6c 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -406,8 +406,6 @@ commands: web: description: Enterprise version of the web app cmd: pnpm --filter @sourcegraph/web dev - watch: - - client/web/dev install: | pnpm install pnpm run generate @@ -974,8 +972,6 @@ bazelCommands: target: //cmd/blobstore:blobstore searcher: target: //cmd/searcher - docsite: - target: //doc:serve syntax-highlighter: target: //docker-images/syntax-highlighter:syntect_server ignoreStdout: true @@ -991,7 +987,6 @@ bazelCommands: QUIET: 'true' frontend: description: Enterprise frontend - continueWatchOnExit: true target: //cmd/frontend precmd: | export SOURCEGRAPH_LICENSE_GENERATION_KEY=$(cat ../dev-private/enterprise/dev/test-license-generation-key.pem) From 3bb5155cf3f4bba6a346aeab7c426eb55bf54e2f Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Fri, 19 Jan 2024 13:31:35 -0800 Subject: [PATCH 09/17] added output buffering --- dev/sg/internal/run/command.go | 47 ++++++++++++++++++++++++++-------- dev/sg/internal/run/ibazel.go | 6 ++++- dev/sg/sg_start.go | 8 ++++-- lib/process/pipe.go | 20 +++++++-------- sg.config.yaml | 2 +- 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 027706f95fa5..639dbf3cdb88 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -224,7 +224,8 @@ type startedCmd struct { outEg *pool.ErrorPool result chan error - opts commandOptions + opts commandOptions + startOutput chan struct{} } func (sc *startedCmd) ErrorChannel() <-chan error { @@ -275,6 +276,17 @@ func (sc *startedCmd) CapturedStderr() string { return string(sc.stderrBuf.Bytes()) } +// Begins writing output to StdOut and StdErr if it was previously buffered +// Errors if command was unbuffered +func (sc *startedCmd) StartOutput() error { + if sc.opts.bufferOutput { + close(sc.startOutput) + return nil + } + + return errors.Newf("cannot start output on unbuffered command: %s", sc.opts.name) +} + func getSecrets(ctx context.Context, name string, extSecrets map[string]secrets.ExternalSecret) (map[string]string, error) { secretsEnv := map[string]string{} @@ -348,9 +360,10 @@ type commandOptions struct { func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { sc := &startedCmd{ - opts: opts, - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, + opts: opts, + stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, + stderrBuf: &prefixSuffixSaver{N: 32 << 10}, + startOutput: make(chan struct{}), } ctx, cancel := context.WithCancel(ctx) @@ -363,13 +376,20 @@ func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { sc.Cmd.Dir = opts.dir sc.Cmd.Env = opts.env - if !opts.bufferOutput { - if err := sc.connectOutput(ctx); err != nil { - return nil, err - } + if err := sc.connectOutput(ctx); err != nil { + sc.cancel() + return nil, err } - return sc, sc.Start() + if !sc.opts.bufferOutput { + close(sc.startOutput) + } + + if err := sc.Start(); err != nil { + sc.cancel() + return nil, err + } + return sc, nil } func (sc *startedCmd) connectOutput(ctx context.Context) error { @@ -398,7 +418,14 @@ func (sc *startedCmd) connectOutput(ctx context.Context) error { stderrWriter = io.MultiWriter(logger, sc.stderrBuf, sgConnLog) } - eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) + // Blocks output until startOutput is signaled + pipe := func(writer io.Writer, reader io.Reader) error { + <-sc.startOutput + return process.DefaultPipe(writer, reader) + + } + + eg, err := process.PipeProcessOutput(ctx, sc.Cmd, stdoutWriter, stderrWriter, pipe) if err != nil { return err } diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index ae0e14d1fba6..50110093a4fc 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -129,7 +129,7 @@ func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { func (ib *IBazel) getCommandOptions(ctx context.Context) commandOptions { return commandOptions{ - name: ib.GetName(), + name: "iBazel", exec: ib.GetExec(ctx), dir: ib.dir, // Don't output iBazel logs until initial build is complete @@ -145,6 +145,10 @@ func (ib *IBazel) Build(ctx context.Context) (err error) { return err } +func (ib *IBazel) StartOutput() error { + return ib.proc.StartOutput() +} + func (ib *IBazel) Stop() { os.RemoveAll(ib.eventsDir) ib.proc.cancel() diff --git a/dev/sg/sg_start.go b/dev/sg/sg_start.go index 8a69b5e0ecb3..a4204851c5c8 100644 --- a/dev/sg/sg_start.go +++ b/dev/sg/sg_start.go @@ -336,19 +336,23 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C installers = append(installers, cmd) } + var ibazel *run.IBazel if len(bcmds) > 0 { - ibazel, err := run.NewIBazel(bcmds, repoRoot) + ibazel, err = run.NewIBazel(bcmds, repoRoot) if err != nil { return err } defer ibazel.Stop() installers = append(installers, ibazel) } - if err := run.Install(ctx, env, verbose, installers...); err != nil { return err } + if ibazel != nil { + ibazel.StartOutput() + } + configCmds := make([]run.SGConfigCommand, 0, len(bcmds)+len(cmds)) for _, cmd := range bcmds { configCmds = append(configCmds, cmd) diff --git a/lib/process/pipe.go b/lib/process/pipe.go index a2a9b23b1ab8..2620590618c5 100644 --- a/lib/process/pipe.go +++ b/lib/process/pipe.go @@ -21,6 +21,15 @@ const maxTokenSize = 100 * 1024 * 1024 // 100mb type pipe func(w io.Writer, r io.Reader) error +func DefaultPipe(w io.Writer, r io.Reader) error { + _, err := io.Copy(w, r) + // We can ignore ErrClosed because we get that if a process crashes + if err != nil && !errors.Is(err, fs.ErrClosed) { + return err + } + return nil +} + type cmdPiper interface { StdoutPipe() (io.ReadCloser, error) StderrPipe() (io.ReadCloser, error) @@ -64,16 +73,7 @@ func PipeOutput(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.W // PipeOutputUnbuffered is the unbuffered version of PipeOutput and uses // io.Copy instead of piping output line-based to the output. func PipeOutputUnbuffered(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer) (*pool.ErrorPool, error) { - pipe := func(w io.Writer, r io.Reader) error { - _, err := io.Copy(w, r) - // We can ignore ErrClosed because we get that if a process crashes - if err != nil && !errors.Is(err, fs.ErrClosed) { - return err - } - return nil - } - - return PipeProcessOutput(ctx, c, stdoutWriter, stderrWriter, pipe) + return PipeProcessOutput(ctx, c, stdoutWriter, stderrWriter, DefaultPipe) } func PipeProcessOutput(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer, fn pipe) (*pool.ErrorPool, error) { diff --git a/sg.config.yaml b/sg.config.yaml index ea831bddff6c..6319b0b37f7f 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -1075,7 +1075,7 @@ commandsets: - ibazel bazelCommands: # - blobstore - - docsite + # - docsite - frontend - worker - repo-updater From cd1787ab991c8d798fa6380f0c2962ddf3475f59 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Mon, 22 Jan 2024 16:31:14 -0800 Subject: [PATCH 10/17] fixed process exiting (I think) --- dev/sg/internal/run/bazel_command.go | 8 +- dev/sg/internal/run/command.go | 38 +++++-- dev/sg/internal/run/ibazel.go | 135 ++++++++++++++---------- dev/sg/internal/run/installer.go | 10 +- dev/sg/internal/run/run.go | 8 +- dev/sg/internal/run/sgconfig_command.go | 14 ++- dev/sg/sg_tests.go | 4 +- 7 files changed, 138 insertions(+), 79 deletions(-) diff --git a/dev/sg/internal/run/bazel_command.go b/dev/sg/internal/run/bazel_command.go index ab8514be90da..d9eeaf74af2c 100644 --- a/dev/sg/internal/run/bazel_command.go +++ b/dev/sg/internal/run/bazel_command.go @@ -6,6 +6,8 @@ import ( "os/exec" "strings" + "github.com/rjeczalik/notify" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" ) @@ -88,11 +90,13 @@ func (bc BazelCommand) StartWatch(ctx context.Context) (<-chan struct{}, error) if watchPaths, err := bc.watchPaths(); err != nil { return nil, err } else { - return WatchPaths(ctx, watchPaths) + // skip remove events as we don't care about files being removed, we only + // want to know when the binary has been rebuilt + return WatchPaths(ctx, watchPaths, notify.Remove) } } -func (bc BazelCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { +func (bc BazelCommand) GetExecCmd(ctx context.Context) (*exec.Cmd, error) { binLocation, err := bc.GetBinaryLocation() if err != nil { return nil, err diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 639dbf3cdb88..828e46446086 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -5,14 +5,15 @@ import ( "fmt" "io" "net" - "os" "os/exec" "path/filepath" + "syscall" "github.com/sourcegraph/conc/pool" "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/dev/sg/interrupt" "github.com/sourcegraph/sourcegraph/dev/sg/root" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" @@ -80,7 +81,7 @@ func (cmd Command) GetEnv() map[string]string { return cmd.Env } -func (cmd Command) GetExec(ctx context.Context) (*exec.Cmd, error) { +func (cmd Command) GetExecCmd(ctx context.Context) (*exec.Cmd, error) { return exec.CommandContext(ctx, "bash", "-c", cmd.Cmd), nil } @@ -124,7 +125,7 @@ func (cmd Command) functionInstall(ctx context.Context, parentEnv map[string]str return nil } -func (cmd Command) watchPaths() ([]string, error) { +func (cmd Command) getWatchPaths() ([]string, error) { root, err := root.RepositoryRoot() if err != nil { return nil, err @@ -139,7 +140,7 @@ func (cmd Command) watchPaths() ([]string, error) { } func (cmd Command) StartWatch(ctx context.Context) (<-chan struct{}, error) { - if watchPaths, err := cmd.watchPaths(); err != nil { + if watchPaths, err := cmd.getWatchPaths(); err != nil { return nil, err } else { return WatchPaths(ctx, watchPaths) @@ -232,7 +233,6 @@ func (sc *startedCmd) ErrorChannel() <-chan error { if sc.result == nil { sc.result = make(chan error) go func() { - defer close(sc.result) sc.result <- sc.Wait() }() } @@ -319,7 +319,7 @@ func OpenUnixSocket() error { } func startSgCmd(ctx context.Context, cmd SGConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { - exec, err := cmd.GetExec(ctx) + exec, err := cmd.GetExecCmd(ctx) if err != nil { return nil, err } @@ -368,14 +368,38 @@ func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { ctx, cancel := context.WithCancel(ctx) sc.cancel = func() { - sc.Cmd.Process.Signal(os.Interrupt) + // The default cancel function will use a SIGKILL (9) which does + // not allow processes to cleanup. If they have spawned child processes + // those child processes will be orphaned and continue running. + // SIGINT will instead gracefully shut down the process and child processes. + if sc.Cmd.Process != nil { + // We created a process group above which we kill here. + pgid, err := syscall.Getpgid(sc.Cmd.Process.Pid) + if err != nil { + // Ignore Errno 3 (No such process) as this means the process has already exited + if !errors.Is(err, syscall.Errno(0x3)) { + panic(errors.Wrapf(err, "failed to get process group ID for %s (PID %d)", sc.opts.name, sc.Cmd.Process.Pid)) + } + // note the minus sign + } else if err := syscall.Kill(-pgid, syscall.SIGINT); err != nil { + panic(errors.Wrapf(err, "failed kill process group ID %d for cmd %s ", pgid, sc.opts.name)) + } + } + cancel() } + // Register an interrput handler + interrupt.Register(sc.cancel) sc.Cmd = opts.exec sc.Cmd.Dir = opts.dir sc.Cmd.Env = opts.env + // This sets up a process group which we kill later. + // This allows us to ensure that any child processes are killed as well when this exits + // This will only work on POSIX systems + sc.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + if err := sc.connectOutput(ctx); err != nil { sc.cancel() return nil, err diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index 50110093a4fc..916978e226af 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -12,19 +12,39 @@ import ( "github.com/nxadm/tail" - "github.com/sourcegraph/conc/pool" - "github.com/sourcegraph/sourcegraph/lib/errors" ) type IBazel struct { targets []string - events chan iBazelEvent + handler *iBazelEventHandler eventsDir string dir string proc *startedCmd } +// returns a runner to interact with ibazel. +func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { + eventsDir, err := os.MkdirTemp("", "ibazel-events") + if err != nil { + return nil, err + } + + targets := make([]string, 0, len(cmds)) + for _, cmd := range cmds { + if !slices.Contains(targets, cmd.Target) { + targets = append(targets, cmd.Target) + } + } + + return &IBazel{ + targets: targets, + handler: newIBazelEventHandler(profileEventsFilePath(eventsDir)), + eventsDir: eventsDir, + dir: dir, + }, nil +} + func (ibazel *IBazel) GetName() string { return fmt.Sprintf("bazel targets (%s)", strings.Join(ibazel.targets, ", ")) } @@ -40,17 +60,13 @@ func (ibazel *IBazel) RunInstall(ctx context.Context, env map[string]string) err return err } - p := pool.New().WithContext(ctx).WithCancelOnError() - - p.Go(func(ctx context.Context) error { - return ibazel.Watch(ctx) - }) + go ibazel.handler.watch(ctx) // block until initial ibazel build is completed return ibazel.WaitForInitialBuild(ctx) } -func (ib *IBazel) GetExec(ctx context.Context) *exec.Cmd { +func (ib *IBazel) GetExecCmd(ctx context.Context) *exec.Cmd { // Writes iBazel events out to a log file. These are much easier to parse // than trying to understand the output directly profilePath := "--profile_dev=" + ib.profileEventsFilePath() @@ -60,35 +76,6 @@ func (ib *IBazel) GetExec(ctx context.Context) *exec.Cmd { return exec.CommandContext(ctx, "ibazel", args...) } -// returns a runner to interact with ibazel. -func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { - eventsDir, err := os.MkdirTemp("", "ibazel-events") - if err != nil { - return nil, err - } - eventsFile, err := os.Create(profileEventsFilePath(eventsDir)) - if err != nil { - return nil, err - } - if err = eventsFile.Close(); err != nil { - return nil, err - } - - targets := make([]string, 0, len(cmds)) - for _, cmd := range cmds { - if !slices.Contains(targets, cmd.Target) { - targets = append(targets, cmd.Target) - } - } - - return &IBazel{ - targets: targets, - events: make(chan iBazelEvent), - eventsDir: eventsDir, - dir: dir, - }, nil -} - func (ib *IBazel) profileEventsFilePath() string { return profileEventsFilePath(ib.eventsDir) } @@ -97,25 +84,9 @@ func profileEventsFilePath(eventsDir string) string { return path.Join(eventsDir, "profile.json") } -// Watch opens the provided profile.json and reads it as it is continuously written by iBazel -// Each time it sees a iBazel event log, it parses it and puts it on the events channel -func (ib *IBazel) Watch(ctx context.Context) error { - tail, err := tail.TailFile(ib.profileEventsFilePath(), tail.Config{Follow: true, ReOpen: true}) - if err != nil { - return err - } - for line := range tail.Lines { - var event iBazelEvent - if err := json.Unmarshal([]byte(line.Text), &event); err != nil { - return errors.Newf("failed to unmarshal event json: %s", err) - } - ib.events <- event - } - return nil -} - func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { - for event := range ib.events { + defer ib.handler.close() + for event := range ib.handler.events { if event.Type == buildDone { return nil } @@ -123,14 +94,13 @@ func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { return errors.Newf("initial ibazel build failed") } } - return nil } func (ib *IBazel) getCommandOptions(ctx context.Context) commandOptions { return commandOptions{ name: "iBazel", - exec: ib.GetExec(ctx), + exec: ib.GetExecCmd(ctx), dir: ib.dir, // Don't output iBazel logs until initial build is complete // as it will break the progress bar @@ -154,6 +124,55 @@ func (ib *IBazel) Stop() { ib.proc.cancel() } +type iBazelEventHandler struct { + events chan iBazelEvent + stop chan struct{} + filename string +} + +func newIBazelEventHandler(filename string) *iBazelEventHandler { + return &iBazelEventHandler{ + events: make(chan iBazelEvent), + stop: make(chan struct{}), + filename: filename, + } +} + +// Watch opens the provided profile.json and reads it as it is continuously written by iBazel +// Each time it sees a iBazel event log, it parses it and puts it on the events channel +// This is a blocking function +func (h *iBazelEventHandler) watch(ctx context.Context) { + _, cancel := context.WithCancelCause(ctx) + tail, err := tail.TailFile(h.filename, tail.Config{Follow: true}) + if err != nil { + cancel(err) + } + defer tail.Cleanup() + defer close(h.events) + defer close(h.stop) + + for { + select { + case line := <-tail.Lines: + var event iBazelEvent + if err := json.Unmarshal([]byte(line.Text), &event); err != nil { + cancel(errors.Newf("failed to unmarshal event json: %s", err)) + } + h.events <- event + case <-ctx.Done(): + cancel(ctx.Err()) + return + case <-h.stop: + return + } + + } +} + +func (h *iBazelEventHandler) close() { + h.stop <- struct{}{} +} + // Schema information at https://github.com/bazelbuild/bazel-watcher?tab=readme-ov-file#profiler-events type iBazelEvent struct { // common diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go index 82d3b6c65d7e..42adf71c1caa 100644 --- a/dev/sg/internal/run/installer.go +++ b/dev/sg/internal/run/installer.go @@ -49,7 +49,7 @@ func Install(ctx context.Context, parentEnv map[string]string, verbose bool, cmd installer.install(ctx, cmds...) - return installer.wait() + return installer.wait(ctx) } func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, verbose bool) *InstallManager { @@ -99,7 +99,7 @@ func (installer *InstallManager) install(ctx context.Context, cmds ...Installer) // Blocks until all installations have successfully completed // or until a failure occurs -func (installer *InstallManager) wait() error { +func (installer *InstallManager) wait(ctx context.Context) error { defer close(installer.installed) defer close(installer.failures) for { @@ -117,6 +117,10 @@ func (installer *InstallManager) wait() error { installer.handleFailure(failure.cmdName, failure.err) return failure + case <-ctx.Done(): + // Context was canceled, exit early + return ctx.Err() + case <-installer.tick(): installer.handleWaiting() } @@ -294,6 +298,7 @@ var installFuncs = map[string]installFunc{ }, } +// As per tradition, if you edit this file you must add a new waiting message var waitingMessages = []string{ "Still waiting for %s to finish installing...", "Yup, still waiting for %s to finish installing...", @@ -311,4 +316,5 @@ var waitingMessages = []string{ "In German there's a saying: ein guter Käse braucht seine Zeit - a good cheese needs its time. Maybe %s is cheese?", "If %ss turns out to be cheese I'm gonna lose it. Hey, hurry up, will ya", "Still waiting for %s to finish installing...", + "You're probably wondering why I've called %s here today...", } diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index a3d868d95988..5595bd2b6246 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -73,7 +73,6 @@ func (runner *cmdRunner) run(ctx context.Context) error { // Wait forever until we're asked to stop or that restarting returns an error. for { - select { // Handle context cancelled case <-ctx.Done(): @@ -81,11 +80,12 @@ func (runner *cmdRunner) run(ctx context.Context) error { // Handle process exit case err := <-sc.ErrorChannel(): - // If the process failed, we exit immedieatly + // If the process failed, we exit immediately if err != nil { return err } - runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.GetName(), output.StyleReset)) + + runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error: %v, %s", output.StyleBold, cmd.GetName(), err, output.StyleReset)) // If we shouldn't restart when the process exits, return if !cmd.GetContinueWatchOnExit() { @@ -243,7 +243,7 @@ func printCmdError(out *output.Output, cmdName string, err error) { } default: - message = fmt.Sprintf("Failed to run %s: %s", cmdName, err) + message = fmt.Sprintf("Failed to run %s: %#v", cmdName, err) } separator := strings.Repeat("-", 80) diff --git a/dev/sg/internal/run/sgconfig_command.go b/dev/sg/internal/run/sgconfig_command.go index ddfda63ae0ee..05e7e2a12b89 100644 --- a/dev/sg/internal/run/sgconfig_command.go +++ b/dev/sg/internal/run/sgconfig_command.go @@ -20,16 +20,20 @@ type SGConfigCommand interface { GetEnv() map[string]string GetBinaryLocation() (string, error) GetExternalSecrets() map[string]secrets.ExternalSecret - GetExec(context.Context) (*exec.Cmd, error) + GetExecCmd(context.Context) (*exec.Cmd, error) // Start a file watcher on the relevant filesystem sub-tree for this command StartWatch(context.Context) (<-chan struct{}, error) } -func WatchPaths(ctx context.Context, paths []string) (<-chan struct{}, error) { +func WatchPaths(ctx context.Context, paths []string, skipEvents ...notify.Event) (<-chan struct{}, error) { // Set up the watchers. restart := make(chan struct{}) events := make(chan notify.EventInfo, 1) + skip := make(HashSet[notify.Event], len(skipEvents)) + for _, event := range skipEvents { + skip[event] = struct{}{} + } // Do nothing if no watch paths are configured if len(paths) == 0 { @@ -51,8 +55,10 @@ func WatchPaths(ctx context.Context, paths []string) (<-chan struct{}, error) { select { case <-ctx.Done(): return - case <-events: - restart <- struct{}{} + case evt := <-events: + if _, shouldSkip := skip[evt.Event()]; !shouldSkip { + restart <- struct{}{} + } } } diff --git a/dev/sg/sg_tests.go b/dev/sg/sg_tests.go index cb1a4368c27c..06a95c7eb683 100644 --- a/dev/sg/sg_tests.go +++ b/dev/sg/sg_tests.go @@ -110,9 +110,9 @@ type sgTestCommand struct { args []string } -// Ovrrides the GetExec method with a custom implementation to construct the command +// Ovrrides the GetExecCmd method with a custom implementation to construct the command // using CLI-passed arguments -func (test sgTestCommand) GetExec(ctx context.Context) (*exec.Cmd, error) { +func (test sgTestCommand) GetExecCmd(ctx context.Context) (*exec.Cmd, error) { cmdArgs := []string{test.Command.Cmd} if len(test.args) != 0 { cmdArgs = append(cmdArgs, test.args...) From 3bd83964d9cf3e6544a7d940a686fe329fb60507 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Tue, 23 Jan 2024 10:19:32 -0800 Subject: [PATCH 11/17] added bazel run targets --- dev/sg/internal/run/bazel_command.go | 20 ++++++++++++++++---- dev/sg/internal/run/command.go | 2 +- dev/sg/internal/run/ibazel.go | 2 +- dev/sg/internal/run/run.go | 17 +++++++++++++++-- sg.config.yaml | 4 +++- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/dev/sg/internal/run/bazel_command.go b/dev/sg/internal/run/bazel_command.go index d9eeaf74af2c..def1737266a2 100644 --- a/dev/sg/internal/run/bazel_command.go +++ b/dev/sg/internal/run/bazel_command.go @@ -27,6 +27,9 @@ type BazelCommand struct { // Preamble is a short and visible message, displayed when the command is launched. Preamble string `yaml:"preamble"` ExternalSecrets map[string]secrets.ExternalSecret `yaml:"external_secrets"` + + // RunTarget specifies a target that should be run via `bazel run $RunTarget` instead of directly executing the binary. + RunTarget string `yaml:"runTarget"` } func (bc BazelCommand) GetName() string { @@ -77,6 +80,10 @@ func (bc BazelCommand) GetExternalSecrets() map[string]secrets.ExternalSecret { } func (bc BazelCommand) watchPaths() ([]string, error) { + // If no target is defined, there is nothing to be built and watched + if bc.Target == "" { + return nil, nil + } // Grab the location of the binary in bazel-out. binLocation, err := bc.GetBinaryLocation() if err != nil { @@ -97,12 +104,17 @@ func (bc BazelCommand) StartWatch(ctx context.Context) (<-chan struct{}, error) } func (bc BazelCommand) GetExecCmd(ctx context.Context) (*exec.Cmd, error) { - binLocation, err := bc.GetBinaryLocation() - if err != nil { - return nil, err + var cmd string + var err error + if bc.RunTarget != "" { + cmd = "bazel run " + bc.RunTarget + } else { + if cmd, err = bc.GetBinaryLocation(); err != nil { + return nil, err + } } - return exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, binLocation)), nil + return exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("%s\n%s", bc.PreCmd, cmd)), nil } func outputPath() ([]byte, error) { diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 828e46446086..34bd01989503 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -380,7 +380,7 @@ func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { if !errors.Is(err, syscall.Errno(0x3)) { panic(errors.Wrapf(err, "failed to get process group ID for %s (PID %d)", sc.opts.name, sc.Cmd.Process.Pid)) } - // note the minus sign + // note the minus sign; this signals that we want to kill the whole process group } else if err := syscall.Kill(-pgid, syscall.SIGINT); err != nil { panic(errors.Wrapf(err, "failed kill process group ID %d for cmd %s ", pgid, sc.opts.name)) } diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index 916978e226af..d2996ab6cca8 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -32,7 +32,7 @@ func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { targets := make([]string, 0, len(cmds)) for _, cmd := range cmds { - if !slices.Contains(targets, cmd.Target) { + if cmd.Target != "" && !slices.Contains(targets, cmd.Target) { targets = append(targets, cmd.Target) } } diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index 5595bd2b6246..2564c86a00b8 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "strings" @@ -85,7 +86,7 @@ func (runner *cmdRunner) run(ctx context.Context) error { return err } - runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error: %v, %s", output.StyleBold, cmd.GetName(), err, output.StyleReset)) + runner.WriteLine(output.Styledf(output.StyleSuccess, "%s%s exited without error%s", output.StyleBold, cmd.GetName(), output.StyleReset)) // If we shouldn't restart when the process exits, return if !cmd.GetContinueWatchOnExit() { @@ -243,7 +244,19 @@ func printCmdError(out *output.Output, cmdName string, err error) { } default: - message = fmt.Sprintf("Failed to run %s: %#v", cmdName, err) + var exc *exec.ExitError + // recurse if it is an exit error + if errors.As(err, &exc) { + printCmdError(out, cmdName, runErr{ + cmdName: cmdName, + exitCode: exc.ExitCode(), + stderr: string(exc.Stderr), + }) + return + } else { + message = fmt.Sprintf("Failed to run %s: %#v", cmdName, err) + } + } separator := strings.Repeat("-", 80) diff --git a/sg.config.yaml b/sg.config.yaml index 6319b0b37f7f..95671ddff224 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -970,6 +970,8 @@ commands: bazelCommands: blobstore: target: //cmd/blobstore:blobstore + docsite: + runTarget: //doc:serve searcher: target: //cmd/searcher syntax-highlighter: @@ -1095,10 +1097,10 @@ commandsets: - caddy simple: bazelCommands: + - docsite - worker commands: - web - - docsite # If you modify this command set, please consider also updating the dotcom runset. enterprise: &enterprise_set requiresDevPrivate: true From 83cb5605f619dd513416fcb2042588b9af6acb54 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Tue, 23 Jan 2024 11:07:27 -0800 Subject: [PATCH 12/17] ran go mod tidy and gazelle --- deps.bzl | 4 ++-- go.mod | 2 +- go.sum | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/deps.bzl b/deps.bzl index c5bf6043b0d9..6af4618a4118 100644 --- a/deps.bzl +++ b/deps.bzl @@ -4698,8 +4698,8 @@ def go_dependencies(): name = "com_github_nxadm_tail", build_file_proto_mode = "disable_global", importpath = "github.com/nxadm/tail", - sum = "h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=", - version = "v1.4.8", + sum = "h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=", + version = "v1.4.11", ) go_repository( name = "com_github_nytimes_gziphandler", diff --git a/go.mod b/go.mod index e5fe39b30ee7..b72da42075c9 100644 --- a/go.mod +++ b/go.mod @@ -267,6 +267,7 @@ require ( github.com/invopop/jsonschema v0.12.0 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/mroth/weightedrand/v2 v2.0.1 + github.com/nxadm/tail v1.4.11 github.com/oschwald/maxminddb-golang v1.12.0 github.com/pkoukk/tiktoken-go v0.1.6 github.com/prometheus/statsd_exporter v0.22.7 @@ -354,7 +355,6 @@ require ( github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nxadm/tail v1.4.11 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/onsi/gomega v1.27.8 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect diff --git a/go.sum b/go.sum index 0e8b4aaa4b2c..3140720cf71a 100644 --- a/go.sum +++ b/go.sum @@ -541,6 +541,7 @@ github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUork github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fullstorydev/grpcui v1.3.1 h1:lVXozTNkJJouBL+wpmvxMnltiwYp8mgyd0TRs93i6Rw= @@ -1403,7 +1404,6 @@ github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnu github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= @@ -2244,6 +2244,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From d94df1f49c8f613e1c8005872f4f2f982879e1e1 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Tue, 23 Jan 2024 21:33:37 -0800 Subject: [PATCH 13/17] fixed installing progress bar render issue --- dev/sg/internal/run/ibazel.go | 4 +--- dev/sg/internal/run/installer.go | 2 +- dev/sg/sg_start_test.go | 1 + sg.config.yaml | 6 ------ 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index d2996ab6cca8..bc88f5cb26ec 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -143,13 +143,11 @@ func newIBazelEventHandler(filename string) *iBazelEventHandler { // This is a blocking function func (h *iBazelEventHandler) watch(ctx context.Context) { _, cancel := context.WithCancelCause(ctx) - tail, err := tail.TailFile(h.filename, tail.Config{Follow: true}) + tail, err := tail.TailFile(h.filename, tail.Config{Follow: true, Logger: tail.DiscardingLogger}) if err != nil { cancel(err) } defer tail.Cleanup() - defer close(h.events) - defer close(h.stop) for { select { diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go index 42adf71c1caa..f2d684517bb0 100644 --- a/dev/sg/internal/run/installer.go +++ b/dev/sg/internal/run/installer.go @@ -73,7 +73,7 @@ func (installer *InstallManager) start(ctx context.Context) { installer.WriteLine(output.Linef(output.EmojiLightbulb, output.StyleBold, "Installing %d commands...", installer.total)) installer.Write("") - installer.progress = std.Out.Progress([]output.ProgressBar{ + installer.progress = installer.Progress([]output.ProgressBar{ {Label: fmt.Sprintf("Installing %d commands", installer.total), Max: float64(installer.total)}, }, nil) diff --git a/dev/sg/sg_start_test.go b/dev/sg/sg_start_test.go index eaddd9ce09c0..80e4b49d3a51 100644 --- a/dev/sg/sg_start_test.go +++ b/dev/sg/sg_start_test.go @@ -45,6 +45,7 @@ func TestStartCommandSet(t *testing.T) { "", "✅ Everything installed! Booting up the system!", "", + "Starting 1 cmds", "Running test-cmd-1...", "[ test-cmd-1] horsegraph booted up. mount your horse.", "[ test-cmd-1] quitting. not horsing around anymore.", diff --git a/sg.config.yaml b/sg.config.yaml index 95671ddff224..3cbcea85affa 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -1095,12 +1095,6 @@ commandsets: - zoekt-web-0 - zoekt-web-1 - caddy - simple: - bazelCommands: - - docsite - - worker - commands: - - web # If you modify this command set, please consider also updating the dotcom runset. enterprise: &enterprise_set requiresDevPrivate: true From feeb869515302ea7523bbe13025ac86e9c145aca Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Wed, 24 Jan 2024 10:38:17 -0800 Subject: [PATCH 14/17] Removed closes that were causing race conditions --- dev/sg/internal/run/installer.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go index f2d684517bb0..74aa7d3d28da 100644 --- a/dev/sg/internal/run/installer.go +++ b/dev/sg/internal/run/installer.go @@ -100,8 +100,6 @@ func (installer *InstallManager) install(ctx context.Context, cmds ...Installer) // Blocks until all installations have successfully completed // or until a failure occurs func (installer *InstallManager) wait(ctx context.Context) error { - defer close(installer.installed) - defer close(installer.failures) for { select { case cmdName := <-installer.installed: From ebc2b71d40ae6edf653db69d387a72ac7fa28332 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 25 Jan 2024 01:27:47 -0800 Subject: [PATCH 15/17] logging extracted from ibazel output --- dev/sg/internal/run/BUILD.bazel | 1 + dev/sg/internal/run/command.go | 293 ++++++++++++--------- dev/sg/internal/run/ibazel.go | 103 ++++++-- dev/sg/internal/run/installer.go | 30 ++- dev/sg/internal/run/logger.go | 10 +- dev/sg/internal/run/prefix_suffix_saver.go | 12 +- dev/sg/internal/run/run.go | 42 +-- dev/sg/sg_start.go | 2 +- lib/process/pipe.go | 20 +- sg.config.yaml | 32 ++- 10 files changed, 339 insertions(+), 206 deletions(-) diff --git a/dev/sg/internal/run/BUILD.bazel b/dev/sg/internal/run/BUILD.bazel index aeeba08d4b76..b726eca1a973 100644 --- a/dev/sg/internal/run/BUILD.bazel +++ b/dev/sg/internal/run/BUILD.bazel @@ -27,6 +27,7 @@ go_library( "//lib/errors", "//lib/output", "//lib/process", + "@com_github_grafana_regexp//:regexp", "@com_github_nxadm_tail//:tail", "@com_github_rjeczalik_notify//:notify", "@com_github_sourcegraph_conc//pool", diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 34bd01989503..98762222d186 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -1,6 +1,7 @@ package run import ( + "bytes" "context" "fmt" "io" @@ -9,6 +10,7 @@ import ( "path/filepath" "syscall" + "github.com/grafana/regexp" "github.com/sourcegraph/conc/pool" "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" @@ -97,6 +99,9 @@ func (cmd Command) RunInstall(ctx context.Context, parentEnv map[string]string) return nil } +// Standard commands ignore installer +func (cmd Command) SetInstallerOutput(chan<- output.FancyLine) {} + func (cmd Command) requiresInstall() bool { return cmd.Install != "" || cmd.InstallFunc != "" } @@ -214,79 +219,14 @@ func equal(a, b []string) bool { return true } -type startedCmd struct { - *exec.Cmd - - cancel func() - - stdoutBuf *prefixSuffixSaver - stderrBuf *prefixSuffixSaver - - outEg *pool.ErrorPool - result chan error - - opts commandOptions - startOutput chan struct{} -} - -func (sc *startedCmd) ErrorChannel() <-chan error { - if sc.result == nil { - sc.result = make(chan error) - go func() { - sc.result <- sc.Wait() - }() - } - return sc.result -} - -func (sc *startedCmd) Wait() error { - err := sc.wait() - var e *exec.ExitError - if errors.As(err, &e) { - err = runErr{ - cmdName: sc.opts.name, - exitCode: e.ExitCode(), - stderr: sc.CapturedStderr(), - stdout: sc.CapturedStdout(), - } - } +var sgConn net.Conn +func OpenUnixSocket() error { + var err error + sgConn, err = net.Dial("unix", "/tmp/sg.sock") return err } -func (sc *startedCmd) wait() error { - if err := sc.outEg.Wait(); err != nil { - return err - } - return sc.Cmd.Wait() -} -func (sc *startedCmd) CapturedStdout() string { - if sc.stdoutBuf == nil { - return "" - } - - return string(sc.stdoutBuf.Bytes()) -} - -func (sc *startedCmd) CapturedStderr() string { - if sc.stderrBuf == nil { - return "" - } - - return string(sc.stderrBuf.Bytes()) -} - -// Begins writing output to StdOut and StdErr if it was previously buffered -// Errors if command was unbuffered -func (sc *startedCmd) StartOutput() error { - if sc.opts.bufferOutput { - close(sc.startOutput) - return nil - } - - return errors.Newf("cannot start output on unbuffered command: %s", sc.opts.name) -} - func getSecrets(ctx context.Context, name string, extSecrets map[string]secrets.ExternalSecret) (map[string]string, error) { secretsEnv := map[string]string{} @@ -310,12 +250,41 @@ func getSecrets(ctx context.Context, name string, extSecrets map[string]secrets. return secretsEnv, errs } -var sgConn net.Conn +type startedCmd struct { + *exec.Cmd + opts commandOptions + cancel func() -func OpenUnixSocket() error { - var err error - sgConn, err = net.Dial("unix", "/tmp/sg.sock") - return err + outEg *pool.ErrorPool + result chan error +} + +type commandOptions struct { + name string + exec *exec.Cmd + dir string + env []string + stdout outputOptions + stderr outputOptions +} + +type outputOptions struct { + // When true, output will be ignored and not written to any writers + ignore bool + + // when enabled, output will not be streamed to the writers until + // after the process is begun, only captured for later retrieval + buffer bool + + // Buffer that captures the output for error logging + captured io.ReadWriter + + // Additional writers to write output to + additionalWriters []io.Writer + + // Channel that is used to signal that output should start streaming + // when buffer is true + start chan struct{} } func startSgCmd(ctx context.Context, cmd SGConfigCommand, dir string, parentEnv map[string]string) (*startedCmd, error) { @@ -331,12 +300,12 @@ func startSgCmd(ctx context.Context, cmd SGConfigCommand, dir string, parentEnv } opts := commandOptions{ - name: cmd.GetName(), - exec: exec, - env: makeEnv(parentEnv, secretsEnv, cmd.GetEnv()), - dir: dir, - ignoreStdOut: cmd.GetIgnoreStdout(), - ignoreStdErr: cmd.GetIgnoreStderr(), + name: cmd.GetName(), + exec: exec, + env: makeEnv(parentEnv, secretsEnv, cmd.GetEnv()), + dir: dir, + stdout: outputOptions{ignore: cmd.GetIgnoreStdout()}, + stderr: outputOptions{ignore: cmd.GetIgnoreStderr()}, } if cmd.GetPreamble() != "" { @@ -346,24 +315,9 @@ func startSgCmd(ctx context.Context, cmd SGConfigCommand, dir string, parentEnv return startCmd(ctx, opts) } -type commandOptions struct { - name string - exec *exec.Cmd - dir string - env []string - ignoreStdOut bool - ignoreStdErr bool - // when enabled, stdout/stderr will not be streamed to the loggers - // after the process is begun, only captured for later retrieval - bufferOutput bool -} - func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { sc := &startedCmd{ - opts: opts, - stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, - stderrBuf: &prefixSuffixSaver{N: 32 << 10}, - startOutput: make(chan struct{}), + opts: opts, } ctx, cancel := context.WithCancel(ctx) @@ -405,10 +359,6 @@ func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { return nil, err } - if !sc.opts.bufferOutput { - close(sc.startOutput) - } - if err := sc.Start(); err != nil { sc.cancel() return nil, err @@ -416,40 +366,44 @@ func startCmd(ctx context.Context, opts commandOptions) (*startedCmd, error) { return sc, nil } -func (sc *startedCmd) connectOutput(ctx context.Context) error { +func (sc *startedCmd) getOutputWriter(ctx context.Context, opts *outputOptions, outputName string) io.Writer { + writers := opts.additionalWriters + if writers == nil { + writers = []io.Writer{} + } + if opts.captured == nil { + opts.captured = &prefixSuffixSaver{N: 32 << 10} + } + writers = append(writers, opts.captured) + + if opts.ignore { + std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring %s of %s", outputName, sc.opts.name)) + } else { + // Create a channel to signal when output should start. If buffering is disabled, close + // the channel so output starts immediately. + opts.start = make(chan struct{}) + if !opts.buffer { + close(opts.start) + } - var stdoutWriter, stderrWriter io.Writer - logger := newCmdLogger(ctx, sc.opts.name, std.Out.Output) + writers = append(writers, newBufferedCmdLogger(ctx, sc.opts.name, std.Out.Output, opts.start)) + } - var sgConnLog io.Writer = io.Discard if sgConn != nil { sink := func(data string) { sgConn.Write([]byte(fmt.Sprintf("%s: %s\n", sc.opts.name, data))) } - sgConnLog = process.NewLogger(ctx, sink) - } - - if sc.opts.ignoreStdOut { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stdout of %s", sc.opts.name)) - stdoutWriter = sc.stdoutBuf - } else { - stdoutWriter = io.MultiWriter(logger, sc.stdoutBuf, sgConnLog) - } - if sc.opts.ignoreStdErr { - std.Out.WriteLine(output.Styledf(output.StyleSuggestion, "Ignoring stderr of %s", sc.opts.name)) - stderrWriter = sc.stderrBuf - } else { - stderrWriter = io.MultiWriter(logger, sc.stderrBuf, sgConnLog) + writers = append(writers, process.NewLogger(ctx, sink)) } - // Blocks output until startOutput is signaled - pipe := func(writer io.Writer, reader io.Reader) error { - <-sc.startOutput - return process.DefaultPipe(writer, reader) + return io.MultiWriter(writers...) +} - } +func (sc *startedCmd) connectOutput(ctx context.Context) error { + stdoutWriter := sc.getOutputWriter(ctx, &sc.opts.stdout, "stdout") + stderrWriter := sc.getOutputWriter(ctx, &sc.opts.stderr, "stderr") - eg, err := process.PipeProcessOutput(ctx, sc.Cmd, stdoutWriter, stderrWriter, pipe) + eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) if err != nil { return err } @@ -457,3 +411,94 @@ func (sc *startedCmd) connectOutput(ctx context.Context) error { return nil } + +func (sc *startedCmd) Exit() <-chan error { + if sc.result == nil { + sc.result = make(chan error) + go func() { + sc.result <- sc.Wait() + }() + } + return sc.result +} + +func (sc *startedCmd) Wait() error { + err := sc.wait() + var e *exec.ExitError + if errors.As(err, &e) { + err = runErr{ + cmdName: sc.opts.name, + exitCode: e.ExitCode(), + stderr: sc.CapturedStderr(), + stdout: sc.CapturedStdout(), + } + } + + return err +} + +func (sc *startedCmd) wait() error { + if err := sc.outEg.Wait(); err != nil { + return err + } + return sc.Cmd.Wait() +} +func (sc *startedCmd) CapturedStdout() string { + return captured(sc.opts.stdout) +} + +func (sc *startedCmd) CapturedStderr() string { + return captured(sc.opts.stderr) +} + +func captured(opts outputOptions) string { + if opts.captured == nil { + return "" + } + + if output, err := io.ReadAll(opts.captured); err == nil { + return string(output) + } + + return "" +} + +// Begins writing output to StdOut and StdErr if it was previously buffered +func (sc *startedCmd) StartOutput() { + sc.startOutput(sc.opts.stdout) + sc.startOutput(sc.opts.stderr) +} + +func (sc *startedCmd) startOutput(opts outputOptions) { + if opts.buffer && opts.start != nil { + close(opts.start) + } +} + +// patternMatcher is writer which looks for a regular expression in the +// written bytes and calls a callback if a match is found +// by default it only looks for the matched pattern once +type patternMatcher struct { + regex *regexp.Regexp + callback func() + buffer bytes.Buffer + multi bool + disabled bool +} + +func (writer *patternMatcher) Write(p []byte) (int, error) { + if writer.disabled { + return len(p), nil + } + n, err := writer.buffer.Write(p) + if err != nil { + return n, err + } + if writer.regex.MatchReader(&writer.buffer) { + writer.callback() + if !writer.multi { + writer.disabled = true + } + } + return n, err +} diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index bc88f5cb26ec..69920a91653d 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -4,28 +4,48 @@ import ( "context" "encoding/json" "fmt" + "io" "os" "os/exec" "path" "slices" "strings" + "github.com/grafana/regexp" "github.com/nxadm/tail" "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/sourcegraph/lib/output" ) +func ibazelLogPath(tempDir string) string { + return path.Join(tempDir, "ibazel.log") +} + +func profileEventsPath(tempDir string) string { + return path.Join(tempDir, "profile.json") +} + +var watchErrorRegex = regexp.MustCompile(`Bazel query failed: exit status 7`) + type IBazel struct { - targets []string - handler *iBazelEventHandler - eventsDir string - dir string - proc *startedCmd + targets []string + events *iBazelEventHandler + tempDir string + logFile *os.File + dir string + proc *startedCmd + logs chan<- output.FancyLine } // returns a runner to interact with ibazel. func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { - eventsDir, err := os.MkdirTemp("", "ibazel-events") + tempDir, err := os.MkdirTemp("", "ibazel") + if err != nil { + return nil, err + } + + logFile, err := os.Create(ibazelLogPath(tempDir)) if err != nil { return nil, err } @@ -38,10 +58,11 @@ func NewIBazel(cmds []BazelCommand, dir string) (*IBazel, error) { } return &IBazel{ - targets: targets, - handler: newIBazelEventHandler(profileEventsFilePath(eventsDir)), - eventsDir: eventsDir, - dir: dir, + targets: targets, + events: newIBazelEventHandler(profileEventsPath(tempDir)), + tempDir: tempDir, + logFile: logFile, + dir: dir, }, nil } @@ -60,33 +81,31 @@ func (ibazel *IBazel) RunInstall(ctx context.Context, env map[string]string) err return err } - go ibazel.handler.watch(ctx) + go ibazel.events.watch(ctx) // block until initial ibazel build is completed return ibazel.WaitForInitialBuild(ctx) } +func (ib *IBazel) SetInstallerOutput(logs chan<- output.FancyLine) { + logs <- output.Styledf(output.StyleGrey, "iBazel output can be found at %s", ibazelLogPath(ib.tempDir)) + logs <- output.Styledf(output.StyleGrey, "iBazel log events can be found at %s", profileEventsPath(ib.tempDir)) + ib.logs = logs +} + func (ib *IBazel) GetExecCmd(ctx context.Context) *exec.Cmd { // Writes iBazel events out to a log file. These are much easier to parse // than trying to understand the output directly - profilePath := "--profile_dev=" + ib.profileEventsFilePath() + profilePath := "--profile_dev=" + profileEventsPath(ib.tempDir) // This enables iBazel to try to apply the fixes from .bazel_fix_commands.json automatically enableAutoFix := "--run_output_interactive=false" args := append([]string{profilePath, enableAutoFix, "build"}, ib.targets...) return exec.CommandContext(ctx, "ibazel", args...) } -func (ib *IBazel) profileEventsFilePath() string { - return profileEventsFilePath(ib.eventsDir) -} - -func profileEventsFilePath(eventsDir string) string { - return path.Join(eventsDir, "profile.json") -} - func (ib *IBazel) WaitForInitialBuild(ctx context.Context) error { - defer ib.handler.close() - for event := range ib.handler.events { + defer ib.events.close() + for event := range ib.events.events { if event.Type == buildDone { return nil } @@ -102,9 +121,14 @@ func (ib *IBazel) getCommandOptions(ctx context.Context) commandOptions { name: "iBazel", exec: ib.GetExecCmd(ctx), dir: ib.dir, - // Don't output iBazel logs until initial build is complete - // as it will break the progress bar - bufferOutput: true, + // Don't output iBazel logs (which are all on stderr) until + // initial build is complete as it will break the progress bar + stderr: outputOptions{ + buffer: true, + additionalWriters: []io.Writer{ + ib.logFile, + &patternMatcher{regex: watchErrorRegex, callback: ib.logWatchError}, + }}, } } @@ -115,15 +139,36 @@ func (ib *IBazel) Build(ctx context.Context) (err error) { return err } -func (ib *IBazel) StartOutput() error { - return ib.proc.StartOutput() +func (ib *IBazel) StartOutput() { + ib.proc.StartOutput() } -func (ib *IBazel) Stop() { - os.RemoveAll(ib.eventsDir) +func (ib *IBazel) Close() { + ib.logFile.Close() + os.RemoveAll(ib.tempDir) ib.proc.cancel() } +func (ib *IBazel) logWatchError() { + buildQuery := `buildfiles(deps(set(%s)))` + queries := make([]string, len(ib.targets)) + for i, target := range ib.targets { + queries[i] = fmt.Sprintf(buildQuery, target) + } + + queryString := strings.Join(queries, " union ") + + msg := `WARNING: iBazel failed to watch for changes, and will be unable to reload upon file changes. +This is likely because bazel query for one of the targets failed. Try running: + +bazel query "%s" + +to determine which target is crashing the analysis. + +` + ib.logs <- output.Styledf(output.StyleWarning, msg, queryString) +} + type iBazelEventHandler struct { events chan iBazelEvent stop chan struct{} diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go index 74aa7d3d28da..55bca4c143d8 100644 --- a/dev/sg/internal/run/installer.go +++ b/dev/sg/internal/run/installer.go @@ -20,6 +20,10 @@ import ( type Installer interface { RunInstall(ctx context.Context, env map[string]string) error + + // Gives a channel which the installer can use to send log messages + SetInstallerOutput(chan<- output.FancyLine) + GetName() string } @@ -33,6 +37,7 @@ type InstallManager struct { // State vars installed chan string failures chan failedRun + logs chan output.FancyLine done int total int waitingMessageIndex int @@ -42,12 +47,12 @@ type InstallManager struct { stats *installAnalytics } -func Install(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...Installer) error { - installer := newInstallManager(cmds, std.Out, parentEnv, verbose) +func Install(ctx context.Context, env map[string]string, verbose bool, cmds ...Installer) error { + installer := newInstallManager(cmds, std.Out, env, verbose) installer.start(ctx) - installer.install(ctx, cmds...) + installer.install(ctx, cmds) return installer.wait(ctx) } @@ -62,6 +67,7 @@ func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, installed: make(chan string, total), failures: make(chan failedRun, total), + logs: make(chan output.FancyLine, 10), done: 0, total: total, } @@ -84,9 +90,12 @@ func (installer *InstallManager) start(ctx context.Context) { } // Starts the installation process in a non-blocking process -func (installer *InstallManager) install(ctx context.Context, cmds ...Installer) { +func (installer *InstallManager) install(ctx context.Context, cmds []Installer) { for _, cmd := range cmds { go func(ctx context.Context, cmd Installer) { + // Set the log channel for the installer + cmd.SetInstallerOutput(installer.logs) + if err := cmd.RunInstall(ctx, installer.env); err != nil { // if failed, put on the failure queue and exit installer.failures <- failedRun{cmdName: cmd.GetName(), err: err} @@ -115,6 +124,9 @@ func (installer *InstallManager) wait(ctx context.Context) error { installer.handleFailure(failure.cmdName, failure.err) return failure + case log := <-installer.logs: + installer.progress.WriteLine(log) + case <-ctx.Done(): // Context was canceled, exit early return ctx.Err() @@ -155,6 +167,16 @@ func (installer *InstallManager) complete() { installer.WriteLine(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Everything installed! Booting up the system!")) } installer.Write("") + + // If there are any pendings logs, print them out + for { + select { + case log := <-installer.logs: + installer.WriteLine(log) + default: + return + } + } } func (installer *InstallManager) handleFailure(name string, err error) { diff --git a/dev/sg/internal/run/logger.go b/dev/sg/internal/run/logger.go index 609e57af45cb..3452dad68495 100644 --- a/dev/sg/internal/run/logger.go +++ b/dev/sg/internal/run/logger.go @@ -31,13 +31,17 @@ var ( lineFormat = "%s%s[%+" + strconv.Itoa(maxNameLength) + "s]%s %s" ) -// newCmdLogger returns a new process.Logger with a unique color based on the name of the cmd. -func newCmdLogger(ctx context.Context, name string, out *output.Output) *process.Logger { +// newBufferedCmdLogger returns a new process.Logger with a unique color based on the name of the cmd +// that blocks until the given start signal and writes logs to the given output.Output. +func newBufferedCmdLogger(ctx context.Context, name string, out *output.Output, start <-chan struct{}) *process.Logger { name = compactName(name) color := nameToColor(name) sink := func(data string) { - out.Writef(lineFormat, output.StyleBold, color, name, output.StyleReset, data) + go func() { + <-start + out.Writef(lineFormat, output.StyleBold, color, name, output.StyleReset, data) + }() } return process.NewLogger(ctx, sink) diff --git a/dev/sg/internal/run/prefix_suffix_saver.go b/dev/sg/internal/run/prefix_suffix_saver.go index ed02f3ed17ec..316be5aca3ab 100644 --- a/dev/sg/internal/run/prefix_suffix_saver.go +++ b/dev/sg/internal/run/prefix_suffix_saver.go @@ -59,12 +59,16 @@ func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) { return p } -func (w *prefixSuffixSaver) Bytes() []byte { +func (w *prefixSuffixSaver) Read(p []byte) (n int, err error) { + return w.Bytes().Read(p) +} + +func (w *prefixSuffixSaver) Bytes() *bytes.Buffer { if w.suffix == nil { - return w.prefix + return bytes.NewBuffer(w.prefix) } if w.skipped == 0 { - return append(w.prefix, w.suffix...) + return bytes.NewBuffer(append(w.prefix, w.suffix...)) } var buf bytes.Buffer buf.Grow(len(w.prefix) + len(w.suffix) + 50) @@ -74,5 +78,5 @@ func (w *prefixSuffixSaver) Bytes() []byte { buf.WriteString(" bytes ...\n") buf.Write(w.suffix[w.suffixOff:]) buf.Write(w.suffix[:w.suffixOff]) - return buf.Bytes() + return &buf } diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index 2564c86a00b8..73b3701c2638 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -18,6 +18,14 @@ import ( "github.com/sourcegraph/sourcegraph/lib/output" ) +type cmdRunner struct { + *std.Output + cmds []SGConfigCommand + repositoryRoot string + parentEnv map[string]string + verbose bool +} + func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...SGConfigCommand) (err error) { if len(cmds) == 0 { // Exit early if there are no commands to run. @@ -65,12 +73,12 @@ func (runner *cmdRunner) run(ctx context.Context) error { } // start up the binary - sc, err := runner.start(ctx, cmd) + proc, err := runner.start(ctx, cmd) if err != nil { runner.printError(cmd, err) return errors.Wrapf(err, "failed to start command %q", cmd.GetName()) } - defer sc.cancel() + defer proc.cancel() // Wait forever until we're asked to stop or that restarting returns an error. for { @@ -80,7 +88,7 @@ func (runner *cmdRunner) run(ctx context.Context) error { return ctx.Err() // Handle process exit - case err := <-sc.ErrorChannel(): + case err := <-proc.Exit(): // If the process failed, we exit immediately if err != nil { return err @@ -105,12 +113,12 @@ func (runner *cmdRunner) run(ctx context.Context) error { if shouldRestart { runner.WriteLine(output.Styledf(output.StylePending, "Restarting %s...", cmd.GetName())) - sc.cancel() - sc, err = runner.start(ctx, cmd) + proc.cancel() + proc, err = runner.start(ctx, cmd) if err != nil { return err } - defer sc.cancel() + defer proc.cancel() } else { runner.WriteLine(output.Styledf(output.StylePending, "Binary for %s did not change. Not restarting.", cmd.GetName())) } @@ -122,14 +130,6 @@ func (runner *cmdRunner) run(ctx context.Context) error { return p.Wait() } -type cmdRunner struct { - *std.Output - cmds []SGConfigCommand - repositoryRoot string - parentEnv map[string]string - verbose bool -} - func (runner *cmdRunner) printError(cmd SGConfigCommand, err error) { printCmdError(runner.Output.Output, cmd.GetName(), err) } @@ -146,7 +146,10 @@ func (runner *cmdRunner) start(ctx context.Context, cmd SGConfigCommand) (*start } func (runner *cmdRunner) reinstall(ctx context.Context, cmd SGConfigCommand) (bool, error) { - if installer, ok := cmd.(Installer); ok { + if installer, ok := cmd.(Installer); !ok { + // If there is no installer, then we always restart + return true, nil + } else { bin, err := cmd.GetBinaryLocation() if err != nil { // If the command doesn't have a CheckBinary, we just ignore it @@ -163,7 +166,7 @@ func (runner *cmdRunner) reinstall(ctx context.Context, cmd SGConfigCommand) (bo } if err := installer.RunInstall(ctx, runner.parentEnv); err != nil { - printCmdError(std.Out.Output, cmd.GetName(), err) + runner.printError(cmd, err) return false, err } newHash, err := md5HashFile(bin) @@ -173,9 +176,6 @@ func (runner *cmdRunner) reinstall(ctx context.Context, cmd SGConfigCommand) (bo return oldHash != newHash, nil } - - // If there is no installer, then we always restart - return true, nil } // failedRun is returned by run when a command failed to run and run exits @@ -357,9 +357,9 @@ func Test(ctx context.Context, cmd SGConfigCommand, parentEnv map[string]string) } std.Out.WriteLine(output.Styledf(output.StylePending, "Starting testsuite %q.", cmd.GetName())) - sc, err := startSgCmd(ctx, cmd, repoRoot, parentEnv) + proc, err := startSgCmd(ctx, cmd, repoRoot, parentEnv) if err != nil { printCmdError(std.Out.Output, cmd.GetName(), err) } - return sc.Wait() + return proc.Wait() } diff --git a/dev/sg/sg_start.go b/dev/sg/sg_start.go index a4204851c5c8..960e473621ee 100644 --- a/dev/sg/sg_start.go +++ b/dev/sg/sg_start.go @@ -342,7 +342,7 @@ func startCommandSet(ctx context.Context, set *sgconf.Commandset, conf *sgconf.C if err != nil { return err } - defer ibazel.Stop() + defer ibazel.Close() installers = append(installers, ibazel) } if err := run.Install(ctx, env, verbose, installers...); err != nil { diff --git a/lib/process/pipe.go b/lib/process/pipe.go index 2620590618c5..a2a9b23b1ab8 100644 --- a/lib/process/pipe.go +++ b/lib/process/pipe.go @@ -21,15 +21,6 @@ const maxTokenSize = 100 * 1024 * 1024 // 100mb type pipe func(w io.Writer, r io.Reader) error -func DefaultPipe(w io.Writer, r io.Reader) error { - _, err := io.Copy(w, r) - // We can ignore ErrClosed because we get that if a process crashes - if err != nil && !errors.Is(err, fs.ErrClosed) { - return err - } - return nil -} - type cmdPiper interface { StdoutPipe() (io.ReadCloser, error) StderrPipe() (io.ReadCloser, error) @@ -73,7 +64,16 @@ func PipeOutput(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.W // PipeOutputUnbuffered is the unbuffered version of PipeOutput and uses // io.Copy instead of piping output line-based to the output. func PipeOutputUnbuffered(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer) (*pool.ErrorPool, error) { - return PipeProcessOutput(ctx, c, stdoutWriter, stderrWriter, DefaultPipe) + pipe := func(w io.Writer, r io.Reader) error { + _, err := io.Copy(w, r) + // We can ignore ErrClosed because we get that if a process crashes + if err != nil && !errors.Is(err, fs.ErrClosed) { + return err + } + return nil + } + + return PipeProcessOutput(ctx, c, stdoutWriter, stderrWriter, pipe) } func PipeProcessOutput(ctx context.Context, c cmdPiper, stdoutWriter, stderrWriter io.Writer, fn pipe) (*pool.ErrorPool, error) { diff --git a/sg.config.yaml b/sg.config.yaml index 075171050620..c354f419eb42 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -413,6 +413,12 @@ commands: env: ENABLE_OPEN_TELEMETRY: true + slow: + description: Nothing + cmd: echo hi + install: | + sleep 25 + web-standalone-http: description: Standalone web frontend (dev) with API proxy to a configurable URL cmd: pnpm --filter @sourcegraph/web serve:dev --color @@ -602,8 +608,6 @@ commands: env: SYNTACTIC_CODE_INTEL_WORKER_ADDR: 127.0.0.1:6076 - - executor-template: &executor_template # TMPDIR is set here so it's not set in the `install` process, which would trip up `go build`. cmd: | @@ -1010,7 +1014,7 @@ commands: bazelCommands: blobstore: - target: //cmd/blobstore:blobstore + target: //cmd/blobstore docsite: runTarget: //doc:serve searcher: @@ -1060,7 +1064,7 @@ bazelCommands: USE_ROCKSKIP: 'false' gitserver-template: &gitserver_bazel_template target: //cmd/gitserver - env: + env: &gitserverenv HOSTNAME: 127.0.0.1:3178 # This is only here to stay backwards-compatible with people's custom # `sg.config.overwrite.yaml` files @@ -1118,8 +1122,8 @@ commandsets: - bazelisk - ibazel bazelCommands: - # - blobstore - # - docsite + - blobstore + - docsite - frontend - worker - repo-updater @@ -1127,16 +1131,25 @@ commandsets: - gitserver-1 - searcher - symbols - # - syntax-highlighter + # - syntax-highlighter commands: - web - # TODO https://github.com/sourcegraph/devx-support/issues/537 - # - docsite - zoekt-index-0 - zoekt-index-1 - zoekt-web-0 - zoekt-web-1 - caddy + + simple: + requiresDevPrivate: true + bazelCommands: + - docsite + - worker + - syntax-highlighter + commands: + - web + - slow + # If you modify this command set, please consider also updating the dotcom runset. enterprise: &enterprise_set requiresDevPrivate: true @@ -1254,7 +1267,6 @@ commandsets: - syntactic-code-intel-worker-0 - syntactic-code-intel-worker-1 - codeintel: requiresDevPrivate: true checks: From 291c219974a47b03167a1a5319a6bc22755b4cd4 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 25 Jan 2024 11:23:16 -0800 Subject: [PATCH 16/17] only log first and relevant errors --- dev/sg/internal/run/command.go | 4 ++++ dev/sg/internal/run/ibazel.go | 8 +++++-- dev/sg/internal/run/installer.go | 30 ++++++++++++------------- dev/sg/internal/run/run.go | 14 +++++++++--- dev/sg/internal/run/sgconfig_command.go | 2 +- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 98762222d186..c3fd4d8469fc 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -102,6 +102,10 @@ func (cmd Command) RunInstall(ctx context.Context, parentEnv map[string]string) // Standard commands ignore installer func (cmd Command) SetInstallerOutput(chan<- output.FancyLine) {} +func (cmd Command) Count() int { + return 1 +} + func (cmd Command) requiresInstall() bool { return cmd.Install != "" || cmd.InstallFunc != "" } diff --git a/dev/sg/internal/run/ibazel.go b/dev/sg/internal/run/ibazel.go index 69920a91653d..e75a4b58c429 100644 --- a/dev/sg/internal/run/ibazel.go +++ b/dev/sg/internal/run/ibazel.go @@ -76,7 +76,7 @@ func (ibazel *IBazel) RunInstall(ctx context.Context, env map[string]string) err return nil } - err := ibazel.Build(ctx) + err := ibazel.build(ctx) if err != nil { return err } @@ -93,6 +93,10 @@ func (ib *IBazel) SetInstallerOutput(logs chan<- output.FancyLine) { ib.logs = logs } +func (ib *IBazel) Count() int { + return len(ib.targets) +} + func (ib *IBazel) GetExecCmd(ctx context.Context) *exec.Cmd { // Writes iBazel events out to a log file. These are much easier to parse // than trying to understand the output directly @@ -134,7 +138,7 @@ func (ib *IBazel) getCommandOptions(ctx context.Context) commandOptions { // Build starts an ibazel process to build the targets provided in the constructor // It runs perpetually, watching for file changes -func (ib *IBazel) Build(ctx context.Context) (err error) { +func (ib *IBazel) build(ctx context.Context) (err error) { ib.proc, err = startCmd(ctx, ib.getCommandOptions(ctx)) return err } diff --git a/dev/sg/internal/run/installer.go b/dev/sg/internal/run/installer.go index 55bca4c143d8..d28a61b11320 100644 --- a/dev/sg/internal/run/installer.go +++ b/dev/sg/internal/run/installer.go @@ -25,12 +25,15 @@ type Installer interface { SetInstallerOutput(chan<- output.FancyLine) GetName() string + + // Number of programs this target is installing + Count() int } type InstallManager struct { // Constructor commands *std.Output - cmds map[string]struct{} + cmds map[string]Installer env map[string]string verbose bool @@ -58,10 +61,15 @@ func Install(ctx context.Context, env map[string]string, verbose bool, cmds ...I } func newInstallManager(cmds []Installer, out *std.Output, env map[string]string, verbose bool) *InstallManager { - total := len(cmds) + total := 0 + cmdsMap := make(map[string]Installer, len(cmds)) + for _, cmd := range cmds { + total += cmd.Count() + cmdsMap[cmd.GetName()] = cmd + } return &InstallManager{ Output: out, - cmds: SliceToHashSet(cmds, func(c Installer) string { return c.GetName() }), + cmds: cmdsMap, verbose: verbose, env: env, @@ -141,7 +149,7 @@ func (installer *InstallManager) startTicker(interval time.Duration) { installer.tickInterval = interval } -func (installer *InstallManager) startAnalytics(ctx context.Context, cmds map[string]struct{}) { +func (installer *InstallManager) startAnalytics(ctx context.Context, cmds map[string]Installer) { installer.stats = startInstallAnalytics(ctx, cmds) } @@ -149,8 +157,8 @@ func (installer *InstallManager) handleInstalled(name string) { installer.stats.handleInstalled(name) installer.ticker.Reset(installer.tickInterval) + installer.done += installer.cmds[name].Count() delete(installer.cmds, name) - installer.done += 1 installer.progress.WriteLine(output.Styledf(output.StyleSuccess, "%s installed", name)) installer.progress.SetValue(0, float64(installer.done)) @@ -214,7 +222,7 @@ type installAnalytics struct { Spans map[string]*analytics.Span } -func startInstallAnalytics(ctx context.Context, cmds map[string]struct{}) *installAnalytics { +func startInstallAnalytics(ctx context.Context, cmds map[string]Installer) *installAnalytics { installer := &installAnalytics{ Start: time.Now(), Spans: make(map[string]*analytics.Span, len(cmds)), @@ -252,16 +260,6 @@ func (a *installAnalytics) duration() time.Duration { return time.Since(a.Start) } -type HashSet[T comparable] map[T]struct{} - -func SliceToHashSet[R any, T comparable](slice []R, extract func(R) T) HashSet[T] { - set := make(HashSet[T], len(slice)) - for _, item := range slice { - set[extract(item)] = struct{}{} - } - return set -} - type installFunc func(context.Context, map[string]string) error var installFuncs = map[string]installFunc{ diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index 73b3701c2638..b14bac649acf 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -46,6 +46,10 @@ func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cm return err } + if err := writePid(); err != nil { + return err + } + runner := cmdRunner{ std.Out, cmds, @@ -58,7 +62,7 @@ func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cm } func (runner *cmdRunner) run(ctx context.Context) error { - p := pool.New().WithContext(ctx).WithCancelOnError() + p := pool.New().WithContext(ctx).WithCancelOnError().WithFirstError() // Start each command concurrently for _, cmd := range runner.cmds { cmd := cmd @@ -134,7 +138,7 @@ func (runner *cmdRunner) printError(cmd SGConfigCommand, err error) { printCmdError(runner.Output.Output, cmd.GetName(), err) } -func (runner *cmdRunner) debug(msg string, args ...any) { +func (runner *cmdRunner) debug(msg string, args ...any) { //nolint currently unused but a handy tool for debugginlg if runner.verbose { message := fmt.Sprintf(msg, args...) runner.WriteLine(output.Styledf(output.StylePending, "%s[DEBUG]: %s %s", output.StyleBold, output.StyleReset, message)) @@ -214,8 +218,12 @@ func (e runErr) Error() string { } func printCmdError(out *output.Output, cmdName string, err error) { - var message, cmdOut string + // Don't log context canceled errors because they are not the root issue + if errors.Is(err, context.Canceled) { + return + } + var message, cmdOut string switch e := errors.Cause(err).(type) { case installErr: message = "Failed to build " + cmdName diff --git a/dev/sg/internal/run/sgconfig_command.go b/dev/sg/internal/run/sgconfig_command.go index 05e7e2a12b89..18216164edab 100644 --- a/dev/sg/internal/run/sgconfig_command.go +++ b/dev/sg/internal/run/sgconfig_command.go @@ -30,7 +30,7 @@ func WatchPaths(ctx context.Context, paths []string, skipEvents ...notify.Event) // Set up the watchers. restart := make(chan struct{}) events := make(chan notify.EventInfo, 1) - skip := make(HashSet[notify.Event], len(skipEvents)) + skip := make(map[notify.Event]struct{}, len(skipEvents)) for _, event := range skipEvents { skip[event] = struct{}{} } From 72958b9e215a5236a1fccb38645e57daf5ff29e2 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Mon, 5 Feb 2024 19:27:52 -0800 Subject: [PATCH 17/17] removed slow testing command --- sg.config.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sg.config.yaml b/sg.config.yaml index 47c0c1b5afb2..e5d6b3a675c9 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -413,12 +413,6 @@ commands: env: ENABLE_OPEN_TELEMETRY: true - slow: - description: Nothing - cmd: echo hi - install: | - sleep 25 - web-standalone-http: description: Standalone web frontend (dev) with API proxy to a configurable URL cmd: pnpm --filter @sourcegraph/web serve:dev --color