diff --git a/internal/boxcli/services.go b/internal/boxcli/services.go index f3ed049d150..e1eb493afe1 100644 --- a/internal/boxcli/services.go +++ b/internal/boxcli/services.go @@ -69,6 +69,15 @@ func servicesCmd(persistentPreRunE ...cobraFunc) *cobra.Command { }, } + attachCommand := &cobra.Command{ + Use: "attach", + Short: "Attach to a running process-compose for the current project", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return attachServices(cmd, flags) + }, + } + lsCommand := &cobra.Command{ Use: "ls", Short: "List available services", @@ -123,6 +132,7 @@ func servicesCmd(persistentPreRunE ...cobraFunc) *cobra.Command { servicesCommand.Flag("run-in-current-shell").Hidden = true serviceUpFlags.register(upCommand) serviceStopFlags.register(stopCommand) + servicesCommand.AddCommand(attachCommand) servicesCommand.AddCommand(lsCommand) servicesCommand.AddCommand(upCommand) servicesCommand.AddCommand(restartCommand) @@ -131,6 +141,19 @@ func servicesCmd(persistentPreRunE ...cobraFunc) *cobra.Command { return servicesCommand } +func attachServices(cmd *cobra.Command, flags servicesCmdFlags) error { + box, err := devbox.Open(&devopt.Opts{ + Dir: flags.config.path, + Environment: flags.config.environment, + Stderr: cmd.ErrOrStderr(), + }) + if err != nil { + return errors.WithStack(err) + } + + return box.AttachToProcessManager(cmd.Context()) +} + func listServices(cmd *cobra.Command, flags servicesCmdFlags) error { box, err := devbox.Open(&devopt.Opts{ Dir: flags.config.path, diff --git a/internal/devbox/services.go b/internal/devbox/services.go index cd9eb02430d..ead19ded6e9 100644 --- a/internal/devbox/services.go +++ b/internal/devbox/services.go @@ -170,6 +170,31 @@ func (d *Devbox) RestartServices( return nil } +func (d *Devbox) AttachToProcessManager(ctx context.Context) error { + if !services.ProcessManagerIsRunning(d.projectDir) { + return usererr.New("Process manager is not running. Run `devbox services up` to start it.") + } + + err := initDevboxUtilityProject(ctx, d.stderr) + if err != nil { + return err + } + + processComposeBinPath, err := utilityLookPath("process-compose") + if err != nil { + return err + } + + return services.AttachToProcessManager( + ctx, + d.stderr, + d.projectDir, + services.ProcessComposeOpts{ + BinPath: processComposeBinPath, + }, + ) +} + func (d *Devbox) StartProcessManager( ctx context.Context, runInCurrentShell bool, diff --git a/internal/services/manager.go b/internal/services/manager.go index 775307d581e..df38f4c4bf6 100644 --- a/internal/services/manager.go +++ b/internal/services/manager.go @@ -159,7 +159,7 @@ func StartProcessManager( if processComposeConfig.Background { flags = append(flags, "-t=false") cmd := exec.Command(processComposeConfig.BinPath, flags...) - return runProcessManagerInBackground(cmd, config, port, projectDir) + return runProcessManagerInBackground(cmd, config, port, projectDir, w) } cmd := exec.Command(processComposeConfig.BinPath, flags...) @@ -206,7 +206,7 @@ func runProcessManagerInForeground(cmd *exec.Cmd, config *globalProcessComposeCo return writeGlobalProcessComposeJSON(config, configFile) } -func runProcessManagerInBackground(cmd *exec.Cmd, config *globalProcessComposeConfig, port int, projectDir string) error { +func runProcessManagerInBackground(cmd *exec.Cmd, config *globalProcessComposeConfig, port int, projectDir string, w io.Writer) error { logdir := filepath.Join(projectDir, processComposeLogfile) logfile, err := os.OpenFile(logdir, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0o664) if err != nil { @@ -216,10 +216,20 @@ func runProcessManagerInBackground(cmd *exec.Cmd, config *globalProcessComposeCo cmd.Stdout = logfile cmd.Stderr = logfile + // These attributes set the process group ID to the process ID of process-compose + // Starting in it's own process group means it won't be terminated if the shell crashes + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pgid: 0, + } + if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start process-compose: %w", err) } + fmt.Fprintf(w, "Process-compose is now running on port %d\n", port) + fmt.Fprintf(w, "To stop your services, run `devbox services stop`\n") + projectConfig := instance{ Pid: cmd.Process.Pid, Port: port, @@ -293,6 +303,30 @@ func StopAllProcessManagers(ctx context.Context, w io.Writer) error { return nil } +func AttachToProcessManager(ctx context.Context, w io.Writer, projectDir string, processComposeConfig ProcessComposeOpts) error { + configFile, err := openGlobalConfigFile() + if err != nil { + return err + } + + defer configFile.Close() + config := readGlobalProcessComposeJSON(configFile) + + project, ok := config.Instances[projectDir] + if !ok { + return fmt.Errorf("process-compose is not running for this project. To start it, run `devbox services up`") + } + + flags := []string{"attach", "-p", strconv.Itoa(project.Port)} + cmd := exec.Command(processComposeConfig.BinPath, flags...) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() +} + func ProcessManagerIsRunning(projectDir string) bool { configFile, err := openGlobalConfigFile() if err != nil {