Skip to content

Commit be47fab

Browse files
authored
fix: properly kill child processes on Windows (#68)
Also add a test that ensures process groups are killed, and create a new unit test action that runs the tests on Ubuntu, MacOS, and Windows.
1 parent b652359 commit be47fab

File tree

7 files changed

+198
-30
lines changed

7 files changed

+198
-30
lines changed

.github/workflows/build.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
on: [push, pull_request]
2-
1+
name: Check compiled action script CI
2+
on:
3+
push:
4+
branches:
5+
- master
6+
pull_request:
37
jobs:
48
compile:
59
runs-on: ubuntu-latest
6-
name: Check compiled script
710
steps:
811
- uses: actions/checkout@v2
912
- name: Setup Node.js

.github/workflows/main.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
on: [push, pull_request]
2-
1+
name: Conformance test framework validation CI
2+
on:
3+
push:
4+
branches:
5+
- master
6+
pull_request:
37
jobs:
48
run-conformance:
59
runs-on: ubuntu-latest
6-
name: Validate conformance test framework
710
steps:
811
- uses: actions/checkout@v2
912

1013
- uses: actions/setup-go@v2
1114
with:
12-
go-version: '^1.13.1'
15+
go-version: '^1.16'
1316

1417
- name: Vendor dependencies
1518
run: "go mod vendor"

.github/workflows/unit.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Unit test CI
2+
on:
3+
push:
4+
branches:
5+
- master
6+
pull_request:
7+
jobs:
8+
test:
9+
strategy:
10+
matrix:
11+
os: [ubuntu-latest, macos-latest, windows-latest]
12+
runs-on: ${{ matrix.os }}
13+
14+
steps:
15+
- name: Check out repo
16+
uses: actions/checkout@v2
17+
18+
- name: Set up Go
19+
uses: actions/setup-go@v2
20+
with:
21+
go-version: '^1.16'
22+
23+
- name: Build and test
24+
run: "go test -v -cover ./..."

client/local.go

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ import (
1818
"io/ioutil"
1919
"log"
2020
"os"
21-
"os/exec"
2221
"strings"
23-
"syscall"
2422
"time"
2523
)
2624

@@ -31,14 +29,7 @@ type localFunctionServer struct {
3129

3230
func (l *localFunctionServer) Start() (func(), error) {
3331
args := strings.Fields(l.cmd)
34-
cmd := exec.Command(args[0], args[1:]...)
35-
36-
// Set a process group ID so that later we can kill child processes too. As an
37-
// example, if the command is `go run main.go`, Go will build a binary in a
38-
// temp dir and then execute it. If we simply cmd.Process.Kill() the exec.Command
39-
// then the running binary will not be killed. Only if we make a group and then
40-
// kill the group will child processes be killed.
41-
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
32+
cmd := newCmd(args)
4233

4334
stdout, err := os.Create(stdoutFile)
4435
if err != nil {
@@ -64,19 +55,8 @@ func (l *localFunctionServer) Start() (func(), error) {
6455
stdout.Close()
6556
stderr.Close()
6657

67-
pgid, err := syscall.Getpgid(cmd.Process.Pid)
68-
if err != nil {
69-
log.Printf("Failed to get pgid: %v", err)
70-
71-
// Kill just the parent process since we failed to get the process group ID.
72-
if err := cmd.Process.Kill(); err != nil {
73-
log.Fatalf("Failed to kill process: %v", err)
74-
}
75-
} else {
76-
// Kill the whole process group.
77-
if err := syscall.Kill(-pgid, syscall.SIGKILL); err != nil {
78-
log.Fatalf("Failed to kill process group: %v", err)
79-
}
58+
if err := stopCmd(cmd); err != nil {
59+
log.Fatalf("Failed to shut down framework server: %v", err)
8060
}
8161

8262
log.Printf("Framework server shut down. Wrote logs to %v and %v.", stdoutFile, stderrFile)

client/local_notwindows.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// +build !windows
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"os/exec"
22+
"syscall"
23+
)
24+
25+
func newCmd(args []string) *exec.Cmd {
26+
cmd := exec.Command(args[0], args[1:]...)
27+
28+
// Set a process group ID so that later we can kill child processes too. As an
29+
// example, if the command is `go run main.go`, Go will build a binary in a
30+
// temp dir and then execute it. If we simply cmd.Process.Kill() the exec.Command
31+
// then the running binary will not be killed. Only if we make a group and then
32+
// kill the group will child processes be killed.
33+
cmd.SysProcAttr = &syscall.SysProcAttr{
34+
Setpgid: true,
35+
}
36+
37+
return cmd
38+
}
39+
40+
func stopCmd(cmd *exec.Cmd) error {
41+
pgid, err := syscall.Getpgid(cmd.Process.Pid)
42+
if err != nil {
43+
// Kill just the parent process since we failed to get the process group ID.
44+
if err := cmd.Process.Kill(); err != nil {
45+
return fmt.Errorf("failed to kill process: %v", err)
46+
}
47+
} else {
48+
// Kill the whole process group.
49+
if err := syscall.Kill(-pgid, syscall.SIGKILL); err != nil {
50+
return fmt.Errorf("failed to kill process group: %v", err)
51+
}
52+
}
53+
54+
return nil
55+
}

client/local_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"fmt"
19+
"io/ioutil"
20+
"path/filepath"
21+
"testing"
22+
)
23+
24+
const testProgram = `package main
25+
26+
import (
27+
"fmt"
28+
"time"
29+
)
30+
31+
func main() {
32+
sleepDuration := time.Second * 90
33+
fmt.Printf("Hello from test program. Sleeping for %v.\n", sleepDuration)
34+
time.Sleep(sleepDuration)
35+
}
36+
`
37+
38+
func TestStartAndShutdown(t *testing.T) {
39+
dir := t.TempDir()
40+
f := filepath.Join(dir, "main.go")
41+
if err := ioutil.WriteFile(f, []byte(testProgram), 0644); err != nil {
42+
t.Fatalf("Failed to write file: %v", err)
43+
}
44+
45+
server := localFunctionServer{
46+
// `go run` compiles the program and then execs it, which allows us to test that
47+
// the whole process group is killed. It's done this way instead of something
48+
// simpler like "/bin/sh -c 'exec sleep 90'" so that it's cross-platform compatible.
49+
cmd: fmt.Sprintf("go run %s", f),
50+
}
51+
52+
shutdown, err := server.Start()
53+
if shutdown != nil {
54+
defer shutdown()
55+
}
56+
57+
if err != nil {
58+
t.Errorf("unable to start localFunctionServer: %v", err)
59+
}
60+
}

client/local_windows.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"fmt"
19+
"os/exec"
20+
"syscall"
21+
)
22+
23+
func newCmd(args []string) *exec.Cmd {
24+
cmd := exec.Command(args[0], args[1:]...)
25+
26+
// Make a process group so that later we can kill child processes too. As an
27+
// example, if the command is `go run main.go`, Go will build a binary in a
28+
// temp dir and then execute it. If we simply cmd.Process.Kill() the exec.Command
29+
// then the running binary will not be killed. Only if we make a group will
30+
// child processes be killed.
31+
cmd.SysProcAttr = &syscall.SysProcAttr{
32+
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
33+
}
34+
35+
return cmd
36+
}
37+
38+
func stopCmd(cmd *exec.Cmd) error {
39+
if err := cmd.Process.Kill(); err != nil {
40+
return fmt.Errorf("failed to kill process: %v", err)
41+
}
42+
return nil
43+
}

0 commit comments

Comments
 (0)