Skip to content

Commit c6f0586

Browse files
authored
Remove use of process groups (#50)
* setgid is needed to set the process group which cannot be used in restricted permission environments
1 parent 3a80009 commit c6f0586

File tree

6 files changed

+178
-92
lines changed

6 files changed

+178
-92
lines changed

init/cli/lib.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func getConfiguredCommands(ctx cli.Context, loggers launchlib.ServiceLoggers) (m
110110
if err != nil {
111111
return nil, errors.Wrap(err, "failed to compile commands from static and custom configurations")
112112
}
113+
113114
cmds := make(map[string]CommandContext)
114115
cmds[staticConfig.ServiceName] = CommandContext{
115116
serviceCmds.Primary,

integration_test/go_java_launcher_integration_test.go

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
package integration_test
1616

1717
import (
18+
"bytes"
1819
"log"
1920
"os"
2021
"os/exec"
2122
"path/filepath"
23+
"regexp"
2224
"strconv"
2325
"strings"
2426
"syscall"
@@ -74,33 +76,39 @@ func TestCreatesDirs(t *testing.T) {
7476
}
7577

7678
func TestSubProcessesStoppedWhenMainDies(t *testing.T) {
77-
cmd := mainWithArgs(t, "testdata/launcher-static-multiprocess.yml", "testdata/launcher-custom-multiprocess-long-sleep.yml")
78-
require.NoError(t, cmd.Start())
79-
80-
// give launcher time to spawn sub-processes
81-
time.Sleep(200 * time.Millisecond)
82-
ppid := cmd.Process.Pid
83-
children := childProcesses(t, ppid)
84-
assert.Len(t, children, 2, "there should be one sub-process and one monitor")
79+
cmd := mainWithArgs(t, "testdata/launcher-static-multiprocess.yml", "testdata/launcher-custom-multiprocess-long-sub-process.yml")
80+
children := runMultiProcess(t, cmd)
8581

8682
assert.NoError(t, cmd.Wait())
87-
8883
time.Sleep(launchlib.CheckPeriod + 500*time.Millisecond)
8984
for _, pid := range children {
90-
// This always succeeds on unix systems as it merely creates a process object
91-
process, err := os.FindProcess(pid)
92-
if err != nil {
93-
continue
94-
}
85+
assert.False(t, launchlib.IsPidAlive(pid), "child was not killed")
86+
}
87+
}
88+
89+
func TestSubProcessesParsedMonitorSignals(t *testing.T) {
90+
cmd := mainWithArgs(t, "testdata/launcher-static-multiprocess.yml", "testdata/launcher-custom-multiprocess-long-sub-process.yml")
91+
92+
output := &bytes.Buffer{}
93+
cmd.Stdout = output
9594

96-
// Sending a signal of 0 checks the process exists, without actually sending a signal,
97-
// see https://linux.die.net/man/2/kill
98-
err = process.Signal(syscall.Signal(0))
99-
if err != nil {
100-
continue
95+
children := runMultiProcess(t, cmd)
96+
var monitor int
97+
for cmdLine, pid := range children {
98+
if strings.Contains(cmdLine, "--group-monitor") {
99+
monitor = pid
100+
break
101101
}
102-
assert.Fail(t, "child process was not killed", pid)
103102
}
103+
104+
assert.NotZero(t, monitor, "no monitor pid found")
105+
require.NoError(t, launchlib.SignalPid(monitor, syscall.SIGPOLL))
106+
107+
assert.NoError(t, cmd.Wait())
108+
109+
trapped, err := regexp.Compile("Caught SIGPOLL")
110+
require.NoError(t, err)
111+
assert.Len(t, trapped.FindAll(output.Bytes(), -1), 2, "expect two messages that SIGPOLL was caught")
104112
}
105113

106114
func runMainWithArgs(t *testing.T, staticConfigFile, customConfigFile string) (string, error) {
@@ -115,8 +123,14 @@ func mainWithArgs(t *testing.T, staticConfigFile, customConfigFile string) *exec
115123
return exec.Command(cli, staticConfigFile, customConfigFile)
116124
}
117125

118-
func childProcesses(t *testing.T, pid int) map[string]int {
119-
command := exec.Command("/bin/ps", "-o", "pid,command", "--no-headers", "--ppid", strconv.Itoa(pid))
126+
func runMultiProcess(t *testing.T, cmd *exec.Cmd) map[string]int {
127+
require.NoError(t, cmd.Start())
128+
129+
// let the launcher create the sub-processes
130+
time.Sleep(500 * time.Millisecond)
131+
ppid := cmd.Process.Pid
132+
133+
command := exec.Command("/bin/ps", "-o", "pid,command", "--no-headers", "--ppid", strconv.Itoa(ppid))
120134
output, err := command.CombinedOutput()
121135
require.NoError(t, err)
122136

@@ -132,6 +146,7 @@ func childProcesses(t *testing.T, pid int) map[string]int {
132146
}
133147
}
134148

149+
assert.Len(t, children, 2, "there should be one sub-process and one monitor")
135150
return children
136151
}
137152

integration_test/jdk/bin/java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
#!/bin/sh
22

3+
trap 'echo "Caught SIGPOLL"' 29
4+
35
echo
46
echo "main method"
57

6-
if [ -z $SLEEP_TIME ]; then
7-
sleep 5
8-
else
9-
sleep $SLEEP_TIME
10-
fi
8+
# sleep stops execution of the current process including echoing that a signal was received
9+
# breaking up the sleep gives time between sleeps for the trap to happen
10+
SLEEP_TIME=${SLEEP_TIME:-5}
11+
COUNTER=0
12+
while [ $COUNTER -lt $SLEEP_TIME ]; do
13+
sleep 1 > /dev/null 2>&1
14+
COUNTER=$((COUNTER+1))
15+
done
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
configType: java
2+
configVersion: 1
3+
jvmOpts:
4+
- '-Xmx1g'
5+
subProcesses:
6+
sidecar:
7+
configType: java
8+
env:
9+
SLEEP_TIME: "200"
10+
jvmOpts:
11+
- '-Xmx1g'

launcher/main/main.go

Lines changed: 49 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,31 @@ func Exit1WithMessage(message string) {
3535
os.Exit(1)
3636
}
3737

38-
func ParseMonitorArgs(args []string) (*launchlib.ProcessMonitor, error) {
38+
func CreateMonitorFromArgs(primaryPID string, subPIDs []string) (*launchlib.ProcessMonitor, error) {
3939
monitor := &launchlib.ProcessMonitor{}
4040

4141
var err error
42-
if monitor.PrimaryPID, err = strconv.Atoi(args[0]); err != nil {
42+
if monitor.PrimaryPID, err = strconv.Atoi(primaryPID); err != nil {
4343
return nil, errors.Wrapf(err, "error parsing service pid")
4444
}
45-
if monitor.ProcessGroupPID, err = strconv.Atoi(args[1]); err != nil {
46-
return nil, errors.Wrapf(err, "error parsing service group id")
45+
46+
for _, pidStr := range subPIDs {
47+
pid, err := strconv.Atoi(pidStr)
48+
if err != nil {
49+
return nil, errors.Wrapf(err, "error parsing sub-process pid")
50+
}
51+
monitor.SubProcessPIDs = append(monitor.SubProcessPIDs, pid)
4752
}
4853
return monitor, nil
4954
}
5055

5156
func GenerateMonitorArgs(monitor *launchlib.ProcessMonitor) []string {
52-
return []string{
53-
monitorFlag,
54-
strconv.Itoa(monitor.PrimaryPID),
55-
strconv.Itoa(monitor.ProcessGroupPID),
57+
args := make([]string, 0, len(monitor.SubProcessPIDs)+2)
58+
args = append(args, monitorFlag, strconv.Itoa(monitor.PrimaryPID))
59+
for _, pid := range monitor.SubProcessPIDs {
60+
args = append(args, strconv.Itoa(pid))
5661
}
62+
return args
5763
}
5864

5965
func main() {
@@ -62,23 +68,27 @@ func main() {
6268
stdout := os.Stdout
6369

6470
switch numArgs := len(os.Args); {
65-
case numArgs == 4 && os.Args[1] == monitorFlag:
66-
if monitor, err := ParseMonitorArgs(os.Args[2:]); err != nil {
71+
case numArgs > 3 && os.Args[1] == monitorFlag:
72+
monitor, err := CreateMonitorFromArgs(os.Args[2], os.Args[3:])
73+
74+
if err != nil {
6775
fmt.Println("error parsing monitor args", err)
68-
Exit1WithMessage(fmt.Sprintf("Usage: go-java-launcher %s <primary pid> <pgid>", monitorFlag))
69-
} else if err = monitor.TermProcessGroupOnDeath(); err != nil {
76+
Exit1WithMessage(fmt.Sprintf("Usage: go-java-launcher %s <primary pid> <sub-process pids...>", monitorFlag))
77+
}
78+
79+
if err = monitor.Run(); err != nil {
7080
fmt.Println("error running process monitor", err)
7181
Exit1WithMessage("process monitor failed")
7282
}
7383
return
74-
case numArgs > 3:
75-
Exit1WithMessage("Usage: go-java-launcher <path to PrimaryStaticLauncherConfig> " +
76-
"[<path to PrimaryCustomLauncherConfig>]")
7784
case numArgs == 2:
7885
staticConfigFile = os.Args[1]
7986
case numArgs == 3:
8087
staticConfigFile = os.Args[1]
8188
customConfigFile = os.Args[2]
89+
default:
90+
Exit1WithMessage("Usage: go-java-launcher <path to PrimaryStaticLauncherConfig> " +
91+
"[<path to PrimaryCustomLauncherConfig>]")
8292
}
8393

8494
// Read configuration
@@ -109,51 +119,43 @@ func main() {
109119
}
110120

111121
if len(cmds.SubProcesses) != 0 {
112-
// For this process (referenced as 0), set the process group id to our pid (also referenced as 0), to
113-
// ensure we are in our own group.
114-
if err := syscall.Setpgid(0, 0); err != nil {
115-
fmt.Printf("Unable to create process group for primary with subProcesses")
116-
panic(err)
117-
}
118-
119-
pgid := syscall.Getpgrp()
120122
monitor := &launchlib.ProcessMonitor{
121-
PrimaryPID: os.Getpid(),
122-
ProcessGroupPID: pgid,
123-
}
124-
monitorCmd := exec.Command(os.Args[0], GenerateMonitorArgs(monitor)...)
125-
monitorCmd.Stdout = os.Stdout
126-
monitorCmd.Stderr = os.Stderr
127-
128-
// From this point, if the launcher, or subsequent primary process dies, the process group will be
129-
// terminated by the process monitor
130-
fmt.Println("Starting process monitor for service process group ", pgid)
131-
if err := monitorCmd.Start(); err != nil {
132-
fmt.Println("Failed to start process monitor for service process group")
133-
panic(err)
123+
PrimaryPID: os.Getpid(),
124+
SubProcessPIDs: nil,
134125
}
126+
// From this point, any errors in the launcher will cause all of the created sub-processes to also die,
127+
// once the main process is exec'ed, this defer will no longer apply, and the external monitor assumes
128+
// responsibility for killing the sub-processes.
129+
defer func() {
130+
if err := monitor.KillSubProcesses(); err != nil {
131+
// Defer only called if failure complete exec of the primary process, so already panicking
132+
fmt.Println("error cleaning up sub-processes", err)
133+
}
134+
}()
135135

136136
for name, subProcess := range cmds.SubProcesses {
137-
// Create struct if not present, as to not override previously set SysProcAttr properties
138-
if subProcess.SysProcAttr == nil {
139-
subProcess.SysProcAttr = &syscall.SysProcAttr{}
140-
}
141-
// Do not set the pgid of the subProcesses, leaving them in the same process group as this one
142-
subProcess.SysProcAttr.Setpgid = false
143137
subProcess.Stdout = os.Stdout
144138
subProcess.Stderr = os.Stderr
145139

146140
fmt.Println("Starting subProcesses ", name, subProcess.Path)
147141
if execErr := subProcess.Start(); execErr != nil {
148142
if os.IsNotExist(execErr) {
149-
fmt.Printf("Executable not found for subProcess %s at: %s\n", name,
150-
subProcess.Path)
143+
fmt.Printf("Executable not found for subProcess %s at: %s\n", name, subProcess.Path)
151144
}
152145
panic(execErr)
153-
} else {
154-
fmt.Printf("Started subProcess %s under process pid %d\n", name,
155-
subProcess.Process.Pid)
156146
}
147+
monitor.SubProcessPIDs = append(monitor.SubProcessPIDs, subProcess.Process.Pid)
148+
fmt.Printf("Started subProcess %s under process pid %d\n", name, subProcess.Process.Pid)
149+
}
150+
151+
monitorCmd := exec.Command(os.Args[0], GenerateMonitorArgs(monitor)...)
152+
monitorCmd.Stdout = os.Stdout
153+
monitorCmd.Stderr = os.Stderr
154+
155+
fmt.Println("Starting process monitor for service process ", monitor.PrimaryPID)
156+
if err := monitorCmd.Start(); err != nil {
157+
fmt.Println("Failed to start process monitor for service process")
158+
panic(err)
157159
}
158160
}
159161

0 commit comments

Comments
 (0)