230 lines
6.4 KiB
Go
230 lines
6.4 KiB
Go
package process
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func (v *VscodeManager) readProcessStderr(stderr io.Reader) {
|
|
scanner := bufio.NewScanner(stderr)
|
|
for scanner.Scan() {
|
|
v.logger.Debug("code-server", zap.String("line", scanner.Text()))
|
|
}
|
|
}
|
|
|
|
func (v *VscodeManager) monitorProcess(cmd *exec.Cmd, doneCh chan struct{}) {
|
|
defer close(doneCh)
|
|
pid := cmd.Process.Pid
|
|
waitErr := cmd.Wait()
|
|
reapErr := v.ensureProcessGroupReaped(pid)
|
|
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
if reapErr != nil {
|
|
v.reapErr = reapErr
|
|
v.logger.Error("code-server process tree was not reaped", zap.Int("pid", pid), zap.Error(reapErr))
|
|
}
|
|
if waitErr != nil && v.status != VscodeStatusStopped {
|
|
v.status = VscodeStatusError
|
|
v.err = waitErr.Error()
|
|
v.logger.Error("code-server exited with error", zap.Int("pid", pid), zap.Error(waitErr))
|
|
return
|
|
}
|
|
v.status = VscodeStatusStopped
|
|
v.logger.Info("code-server exited", zap.Int("pid", pid))
|
|
}
|
|
|
|
// Stop stops the code-server process.
|
|
func (v *VscodeManager) Stop(ctx context.Context) error {
|
|
startDone := v.beginStop()
|
|
if err := waitForVscodeStartGeneration(ctx, startDone); err != nil {
|
|
return err
|
|
}
|
|
cmd, doneCh := v.snapshotStoppedProcess(startDone)
|
|
if cmd != nil && cmd.Process != nil {
|
|
return v.stopRunningProcess(ctx, cmd.Process.Pid, doneCh)
|
|
}
|
|
if doneCh != nil {
|
|
return waitForVscodeStartupReap(ctx, doneCh)
|
|
}
|
|
v.logger.Info("code-server stopped")
|
|
return nil
|
|
}
|
|
|
|
func (v *VscodeManager) beginStop() chan struct{} {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
startDone := v.startDone
|
|
cancelStart := v.cancelStart
|
|
v.cancelStart = nil
|
|
if cancelStart != nil {
|
|
cancelStart()
|
|
}
|
|
if v.afterStartCancel != nil {
|
|
v.afterStartCancel()
|
|
}
|
|
v.logger.Info("stopping code-server")
|
|
v.status = VscodeStatusStopped
|
|
if v.stopCh != nil && !v.stopped {
|
|
close(v.stopCh)
|
|
v.stopped = true
|
|
}
|
|
return startDone
|
|
}
|
|
|
|
func waitForVscodeStartGeneration(ctx context.Context, startDone <-chan struct{}) error {
|
|
if startDone != nil {
|
|
select {
|
|
case <-startDone:
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("wait for code-server startup generation: %w", ctx.Err())
|
|
case <-time.After(2 * time.Second):
|
|
return fmt.Errorf("code-server startup generation did not stop after cancellation")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *VscodeManager) snapshotStoppedProcess(startDone chan struct{}) (*exec.Cmd, <-chan struct{}) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
cmd := v.cmd
|
|
doneCh := v.doneCh
|
|
if v.startDone == startDone {
|
|
v.startDone = nil
|
|
}
|
|
v.status = VscodeStatusStopped
|
|
return cmd, doneCh
|
|
}
|
|
|
|
func (v *VscodeManager) stopRunningProcess(ctx context.Context, pid int, doneCh <-chan struct{}) error {
|
|
v.logger.Info("stopping code-server process group", zap.Int("pid", pid), zap.Int("pgid", pid))
|
|
logChildProcesses(v.logger, pid)
|
|
v.logger.Debug("code-server process group SIGTERM requested",
|
|
zap.Int("pgid", pid), zap.String("reason", "stop_requested"))
|
|
if err := v.terminateProcessGroup(pid); err != nil {
|
|
v.logger.Warn("failed to send SIGTERM to code-server group", zap.Int("pgid", pid), zap.Error(err))
|
|
}
|
|
if doneCh == nil {
|
|
return v.ensureProcessGroupReaped(pid)
|
|
}
|
|
select {
|
|
case <-doneCh:
|
|
v.logger.Info("code-server stopped gracefully", zap.Int("pid", pid))
|
|
return v.ensureProcessGroupReaped(pid)
|
|
case <-ctx.Done():
|
|
v.logger.Warn("context cancelled during SIGTERM wait, force killing", zap.Int("pgid", pid))
|
|
case <-time.After(5 * time.Second):
|
|
v.logger.Warn("code-server did not exit after SIGTERM, force killing", zap.Int("pgid", pid))
|
|
}
|
|
return v.forceStopRunningProcess(ctx, pid, doneCh)
|
|
}
|
|
|
|
func (v *VscodeManager) forceStopRunningProcess(ctx context.Context, pid int, doneCh <-chan struct{}) error {
|
|
v.logger.Debug("code-server process group SIGKILL requested",
|
|
zap.Int("pgid", pid), zap.String("reason", "grace_expired_or_context_canceled"))
|
|
if err := v.killProcessGroup(pid); err != nil {
|
|
v.logger.Warn("failed to force kill code-server group", zap.Int("pgid", pid), zap.Error(err))
|
|
}
|
|
select {
|
|
case <-doneCh:
|
|
return v.ensureProcessGroupReaped(pid)
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("wait for code-server process reap: %w", ctx.Err())
|
|
case <-time.After(2 * time.Second):
|
|
v.logger.Error("code-server still alive after SIGKILL", zap.Int("pid", pid))
|
|
return fmt.Errorf("code-server process %d was not reaped after force kill", pid)
|
|
}
|
|
}
|
|
|
|
func waitForVscodeStartupReap(ctx context.Context, doneCh <-chan struct{}) error {
|
|
select {
|
|
case <-doneCh:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("wait for code-server startup reap: %w", ctx.Err())
|
|
case <-time.After(2 * time.Second):
|
|
return fmt.Errorf("code-server startup process was not reaped after stop")
|
|
}
|
|
}
|
|
|
|
func (v *VscodeManager) ensureProcessGroupReaped(pid int) error {
|
|
v.mu.Lock()
|
|
lifecycle := v.lifecycle
|
|
v.mu.Unlock()
|
|
if err := reapProcessLifecycle(lifecycle); err != nil {
|
|
return fmt.Errorf("reap code-server process job: %w", err)
|
|
}
|
|
if !v.processGroupAlive(pid) {
|
|
v.clearReapedProcessOwnership(lifecycle)
|
|
return nil
|
|
}
|
|
_ = v.killProcessGroup(pid)
|
|
ctx, cancel := context.WithTimeout(context.Background(), processGroupTerminateGrace)
|
|
defer cancel()
|
|
if v.waitForProcessGroupExit(ctx, pid) {
|
|
v.clearReapedProcessOwnership(lifecycle)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("code-server process group %d remains alive", pid)
|
|
}
|
|
|
|
func (v *VscodeManager) clearReapedProcessOwnership(lifecycle processLifecycleHandle) {
|
|
v.mu.Lock()
|
|
if v.lifecycle == lifecycle {
|
|
v.lifecycle = processLifecycleHandle{}
|
|
v.reapErr = nil
|
|
}
|
|
v.mu.Unlock()
|
|
}
|
|
|
|
func (v *VscodeManager) processGroupAlive(pid int) bool {
|
|
if v.groupAliveFn != nil {
|
|
return v.groupAliveFn(pid)
|
|
}
|
|
return processGroupAlive(pid)
|
|
}
|
|
|
|
func (v *VscodeManager) killProcessGroup(pid int) error {
|
|
if v.killGroupFn != nil {
|
|
return v.killGroupFn(pid)
|
|
}
|
|
return killProcessGroup(pid)
|
|
}
|
|
|
|
func (v *VscodeManager) terminateProcessGroup(pid int) error {
|
|
if v.terminateGroupFn != nil {
|
|
return v.terminateGroupFn(pid)
|
|
}
|
|
return terminateProcessGroup(pid)
|
|
}
|
|
|
|
func (v *VscodeManager) waitForProcessGroupExit(ctx context.Context, pid int) bool {
|
|
if v.waitGroupExitFn != nil {
|
|
return v.waitGroupExitFn(ctx, pid)
|
|
}
|
|
return waitForProcessGroupExit(ctx, pid)
|
|
}
|
|
|
|
func (v *VscodeManager) HasUnreapedOwnership() bool {
|
|
v.mu.Lock()
|
|
startDone := v.startDone
|
|
cmd := v.cmd
|
|
reapErr := v.reapErr
|
|
v.mu.Unlock()
|
|
if startDone != nil {
|
|
select {
|
|
case <-startDone:
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
return reapErr != nil || (cmd != nil && cmd.Process != nil && v.processGroupAlive(cmd.Process.Pid))
|
|
}
|