Skip to content

Commit

Permalink
[Services] Keep Process Compose alive in background, add attach com…
Browse files Browse the repository at this point in the history
…mand (#2269)

## Summary

This fixes two previously reported issues: 

1. If process-compose is not terminated gracefully (e.g., it's parent
shell crashes, or is closed on accident), then process-compose may
terminate without also terminating it's services.
2. If process-compose is started in the background, a user can now run
the `attach` command to re-attach the TUI to the backgrounded process
compose

A few current limitations: 

1. This only applies to `devbox services up -b`, but could be extended
to `devbox services up` as well by starting process-compose in the
background, and then attaching the TUI
2. You will now need to explicitly run `devbox services stop` to stop
process-compose for your project

Todos: 

1. Should we apply the backgrounding to `devbox services up` as well?
2. Should we allow users to specify a port or specific process-compose
instance to attach to?
3. If so, should we list the process-compose instances somewhere? 

## How was it tested?

Tested on the Apache example:

Attach: 
1. Run `devbox services up -b` in the apache folder
2. Run `devbox services attach` in the apache folder, verify that it
launches the TUI
3. Hit Ctrl-C to exit the TUI

Backgrounding:
4. Run `devbox services list`, verify that process-compose is still
running
5. Terminate your shell or editor
6. Launch a new shell or editor, navigate to the apache example
7. Run `devbox services list` to verify that process-compose is still
running.
  • Loading branch information
Lagoja authored Sep 13, 2024
1 parent df187b9 commit 3ac5262
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 2 deletions.
23 changes: 23 additions & 0 deletions internal/boxcli/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions internal/devbox/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 36 additions & 2 deletions internal/services/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 3ac5262

Please sign in to comment.