Skip to content

Commit 3ac5262

Browse files
authored
[Services] Keep Process Compose alive in background, add attach command (#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.
1 parent df187b9 commit 3ac5262

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed

internal/boxcli/services.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ func servicesCmd(persistentPreRunE ...cobraFunc) *cobra.Command {
6969
},
7070
}
7171

72+
attachCommand := &cobra.Command{
73+
Use: "attach",
74+
Short: "Attach to a running process-compose for the current project",
75+
Args: cobra.ExactArgs(0),
76+
RunE: func(cmd *cobra.Command, args []string) error {
77+
return attachServices(cmd, flags)
78+
},
79+
}
80+
7281
lsCommand := &cobra.Command{
7382
Use: "ls",
7483
Short: "List available services",
@@ -123,6 +132,7 @@ func servicesCmd(persistentPreRunE ...cobraFunc) *cobra.Command {
123132
servicesCommand.Flag("run-in-current-shell").Hidden = true
124133
serviceUpFlags.register(upCommand)
125134
serviceStopFlags.register(stopCommand)
135+
servicesCommand.AddCommand(attachCommand)
126136
servicesCommand.AddCommand(lsCommand)
127137
servicesCommand.AddCommand(upCommand)
128138
servicesCommand.AddCommand(restartCommand)
@@ -131,6 +141,19 @@ func servicesCmd(persistentPreRunE ...cobraFunc) *cobra.Command {
131141
return servicesCommand
132142
}
133143

144+
func attachServices(cmd *cobra.Command, flags servicesCmdFlags) error {
145+
box, err := devbox.Open(&devopt.Opts{
146+
Dir: flags.config.path,
147+
Environment: flags.config.environment,
148+
Stderr: cmd.ErrOrStderr(),
149+
})
150+
if err != nil {
151+
return errors.WithStack(err)
152+
}
153+
154+
return box.AttachToProcessManager(cmd.Context())
155+
}
156+
134157
func listServices(cmd *cobra.Command, flags servicesCmdFlags) error {
135158
box, err := devbox.Open(&devopt.Opts{
136159
Dir: flags.config.path,

internal/devbox/services.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,31 @@ func (d *Devbox) RestartServices(
170170
return nil
171171
}
172172

173+
func (d *Devbox) AttachToProcessManager(ctx context.Context) error {
174+
if !services.ProcessManagerIsRunning(d.projectDir) {
175+
return usererr.New("Process manager is not running. Run `devbox services up` to start it.")
176+
}
177+
178+
err := initDevboxUtilityProject(ctx, d.stderr)
179+
if err != nil {
180+
return err
181+
}
182+
183+
processComposeBinPath, err := utilityLookPath("process-compose")
184+
if err != nil {
185+
return err
186+
}
187+
188+
return services.AttachToProcessManager(
189+
ctx,
190+
d.stderr,
191+
d.projectDir,
192+
services.ProcessComposeOpts{
193+
BinPath: processComposeBinPath,
194+
},
195+
)
196+
}
197+
173198
func (d *Devbox) StartProcessManager(
174199
ctx context.Context,
175200
runInCurrentShell bool,

internal/services/manager.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func StartProcessManager(
159159
if processComposeConfig.Background {
160160
flags = append(flags, "-t=false")
161161
cmd := exec.Command(processComposeConfig.BinPath, flags...)
162-
return runProcessManagerInBackground(cmd, config, port, projectDir)
162+
return runProcessManagerInBackground(cmd, config, port, projectDir, w)
163163
}
164164

165165
cmd := exec.Command(processComposeConfig.BinPath, flags...)
@@ -206,7 +206,7 @@ func runProcessManagerInForeground(cmd *exec.Cmd, config *globalProcessComposeCo
206206
return writeGlobalProcessComposeJSON(config, configFile)
207207
}
208208

209-
func runProcessManagerInBackground(cmd *exec.Cmd, config *globalProcessComposeConfig, port int, projectDir string) error {
209+
func runProcessManagerInBackground(cmd *exec.Cmd, config *globalProcessComposeConfig, port int, projectDir string, w io.Writer) error {
210210
logdir := filepath.Join(projectDir, processComposeLogfile)
211211
logfile, err := os.OpenFile(logdir, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0o664)
212212
if err != nil {
@@ -216,10 +216,20 @@ func runProcessManagerInBackground(cmd *exec.Cmd, config *globalProcessComposeCo
216216
cmd.Stdout = logfile
217217
cmd.Stderr = logfile
218218

219+
// These attributes set the process group ID to the process ID of process-compose
220+
// Starting in it's own process group means it won't be terminated if the shell crashes
221+
cmd.SysProcAttr = &syscall.SysProcAttr{
222+
Setpgid: true,
223+
Pgid: 0,
224+
}
225+
219226
if err := cmd.Start(); err != nil {
220227
return fmt.Errorf("failed to start process-compose: %w", err)
221228
}
222229

230+
fmt.Fprintf(w, "Process-compose is now running on port %d\n", port)
231+
fmt.Fprintf(w, "To stop your services, run `devbox services stop`\n")
232+
223233
projectConfig := instance{
224234
Pid: cmd.Process.Pid,
225235
Port: port,
@@ -293,6 +303,30 @@ func StopAllProcessManagers(ctx context.Context, w io.Writer) error {
293303
return nil
294304
}
295305

306+
func AttachToProcessManager(ctx context.Context, w io.Writer, projectDir string, processComposeConfig ProcessComposeOpts) error {
307+
configFile, err := openGlobalConfigFile()
308+
if err != nil {
309+
return err
310+
}
311+
312+
defer configFile.Close()
313+
config := readGlobalProcessComposeJSON(configFile)
314+
315+
project, ok := config.Instances[projectDir]
316+
if !ok {
317+
return fmt.Errorf("process-compose is not running for this project. To start it, run `devbox services up`")
318+
}
319+
320+
flags := []string{"attach", "-p", strconv.Itoa(project.Port)}
321+
cmd := exec.Command(processComposeConfig.BinPath, flags...)
322+
323+
cmd.Stdout = os.Stdout
324+
cmd.Stderr = os.Stderr
325+
cmd.Stdin = os.Stdin
326+
327+
return cmd.Run()
328+
}
329+
296330
func ProcessManagerIsRunning(projectDir string) bool {
297331
configFile, err := openGlobalConfigFile()
298332
if err != nil {

0 commit comments

Comments
 (0)