Browse code

Change 'docker run' exit codes to distinguish docker/contained errors

The purpose of this PR is for users to distinguish Docker errors from
contained command errors.
This PR modifies 'docker run' exit codes to follow the chroot standard
for exit codes.
Exit status:
125 if 'docker run' itself fails
126 if contained command cannot be invoked
127 if contained command cannot be found
the exit status otherwise

Signed-off-by: Sally O'Malley <somalley@redhat.com>

Sally O'Malley authored on 2015/07/29 21:21:16
Showing 11 changed files
... ...
@@ -6,9 +6,11 @@ import (
6 6
 	"net/url"
7 7
 	"os"
8 8
 	"runtime"
9
+	"strings"
9 10
 
10 11
 	"github.com/Sirupsen/logrus"
11 12
 	Cli "github.com/docker/docker/cli"
13
+	derr "github.com/docker/docker/errors"
12 14
 	"github.com/docker/docker/opts"
13 15
 	"github.com/docker/docker/pkg/promise"
14 16
 	"github.com/docker/docker/pkg/signal"
... ...
@@ -36,6 +38,29 @@ func (cid *cidFile) Write(id string) error {
36 36
 	return nil
37 37
 }
38 38
 
39
+// if container start fails with 'command not found' error, return 127
40
+// if container start fails with 'command cannot be invoked' error, return 126
41
+// return 125 for generic docker daemon failures
42
+func runStartContainerErr(err error) error {
43
+	trimmedErr := strings.Trim(err.Error(), "Error response from daemon: ")
44
+	statusError := Cli.StatusError{}
45
+	derrCmdNotFound := derr.ErrorCodeCmdNotFound.Message()
46
+	derrCouldNotInvoke := derr.ErrorCodeCmdCouldNotBeInvoked.Message()
47
+	derrNoSuchImage := derr.ErrorCodeNoSuchImageHash.Message()
48
+	derrNoSuchImageTag := derr.ErrorCodeNoSuchImageTag.Message()
49
+	switch trimmedErr {
50
+	case derrCmdNotFound:
51
+		statusError = Cli.StatusError{StatusCode: 127}
52
+	case derrCouldNotInvoke:
53
+		statusError = Cli.StatusError{StatusCode: 126}
54
+	case derrNoSuchImage, derrNoSuchImageTag:
55
+		statusError = Cli.StatusError{StatusCode: 125}
56
+	default:
57
+		statusError = Cli.StatusError{StatusCode: 125}
58
+	}
59
+	return statusError
60
+}
61
+
39 62
 // CmdRun runs a command in a new container.
40 63
 //
41 64
 // Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
... ...
@@ -60,7 +85,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
60 60
 	// just in case the Parse does not exit
61 61
 	if err != nil {
62 62
 		cmd.ReportError(err.Error(), true)
63
-		os.Exit(1)
63
+		os.Exit(125)
64 64
 	}
65 65
 
66 66
 	if len(hostConfig.DNS) > 0 {
... ...
@@ -115,7 +140,8 @@ func (cli *DockerCli) CmdRun(args ...string) error {
115 115
 
116 116
 	createResponse, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName)
117 117
 	if err != nil {
118
-		return err
118
+		cmd.ReportError(err.Error(), true)
119
+		return runStartContainerErr(err)
119 120
 	}
120 121
 	if sigProxy {
121 122
 		sigc := cli.forwardAllSignals(createResponse.ID)
... ...
@@ -199,8 +225,9 @@ func (cli *DockerCli) CmdRun(args ...string) error {
199 199
 	}()
200 200
 
201 201
 	//start the container
202
-	if _, _, err = readBody(cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)); err != nil {
203
-		return err
202
+	if _, _, err := readBody(cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)); err != nil {
203
+		cmd.ReportError(err.Error(), false)
204
+		return runStartContainerErr(err)
204 205
 	}
205 206
 
206 207
 	if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminalOut {
... ...
@@ -230,7 +257,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
230 230
 		// Autoremove: wait for the container to finish, retrieve
231 231
 		// the exit code and remove the container
232 232
 		if _, _, err := readBody(cli.call("POST", "/containers/"+createResponse.ID+"/wait", nil, nil)); err != nil {
233
-			return err
233
+			return runStartContainerErr(err)
234 234
 		}
235 235
 		if _, status, err = getExitCode(cli, createResponse.ID); err != nil {
236 236
 			return err
... ...
@@ -3,13 +3,17 @@ package daemon
3 3
 import (
4 4
 	"io"
5 5
 	"os/exec"
6
+	"strings"
6 7
 	"sync"
8
+	"syscall"
7 9
 	"time"
8 10
 
9 11
 	"github.com/Sirupsen/logrus"
10 12
 	"github.com/docker/docker/daemon/execdriver"
13
+	derr "github.com/docker/docker/errors"
11 14
 	"github.com/docker/docker/pkg/stringid"
12 15
 	"github.com/docker/docker/runconfig"
16
+	"github.com/docker/docker/utils"
13 17
 )
14 18
 
15 19
 const (
... ...
@@ -163,11 +167,30 @@ func (m *containerMonitor) Start() error {
163 163
 		if exitStatus, err = m.supervisor.Run(m.container, pipes, m.callback); err != nil {
164 164
 			// if we receive an internal error from the initial start of a container then lets
165 165
 			// return it instead of entering the restart loop
166
+			// set to 127 for contained cmd not found/does not exist)
167
+			if strings.Contains(err.Error(), "executable file not found") ||
168
+				strings.Contains(err.Error(), "no such file or directory") ||
169
+				strings.Contains(err.Error(), "system cannot find the file specified") {
170
+				if m.container.RestartCount == 0 {
171
+					m.container.ExitCode = 127
172
+					m.resetContainer(false)
173
+					return derr.ErrorCodeCmdNotFound
174
+				}
175
+			}
176
+			// set to 126 for contained cmd can't be invoked errors
177
+			if strings.Contains(err.Error(), syscall.EACCES.Error()) {
178
+				if m.container.RestartCount == 0 {
179
+					m.container.ExitCode = 126
180
+					m.resetContainer(false)
181
+					return derr.ErrorCodeCmdCouldNotBeInvoked
182
+				}
183
+			}
184
+
166 185
 			if m.container.RestartCount == 0 {
167 186
 				m.container.ExitCode = -1
168 187
 				m.resetContainer(false)
169 188
 
170
-				return err
189
+				return derr.ErrorCodeCantStart.WithArgs(utils.GetErrorMessage(err))
171 190
 			}
172 191
 
173 192
 			logrus.Errorf("Error running container: %s", err)
... ...
@@ -7,7 +7,6 @@ import (
7 7
 	derr "github.com/docker/docker/errors"
8 8
 	"github.com/docker/docker/pkg/promise"
9 9
 	"github.com/docker/docker/runconfig"
10
-	"github.com/docker/docker/utils"
11 10
 )
12 11
 
13 12
 // ContainerStart starts a container.
... ...
@@ -47,7 +46,7 @@ func (daemon *Daemon) ContainerStart(name string, hostConfig *runconfig.HostConf
47 47
 	}
48 48
 
49 49
 	if err := daemon.containerStart(container); err != nil {
50
-		return derr.ErrorCodeCantStart.WithArgs(name, utils.GetErrorMessage(err))
50
+		return err
51 51
 	}
52 52
 
53 53
 	return nil
... ...
@@ -518,6 +518,38 @@ non-zero exit status more than 10 times in a row Docker will abort trying to
518 518
 restart the container. Providing a maximum restart limit is only valid for the
519 519
 **on-failure** policy.
520 520
 
521
+## Exit Status
522
+
523
+The exit code from `docker run` gives information about why the container
524
+failed to run or why it exited.  When `docker run` exits with a non-zero code,
525
+the exit codes follow the `chroot` standard, see below:
526
+
527
+**_125_** if the error is with Docker daemon **_itself_** 
528
+
529
+    $ docker run --foo busybox; echo $?
530
+    # flag provided but not defined: --foo
531
+      See 'docker run --help'.
532
+      125
533
+
534
+**_126_** if the **_contained command_** cannot be invoked
535
+
536
+    $ docker run busybox /etc; echo $?
537
+    # exec: "/etc": permission denied
538
+      docker: Error response from daemon: Contained command could not be invoked
539
+      126
540
+
541
+**_127_** if the **_contained command_** cannot be found
542
+
543
+    $ docker run busybox foo; echo $?
544
+    # exec: "foo": executable file not found in $PATH
545
+      docker: Error response from daemon: Contained command not found or does not exist
546
+      127
547
+
548
+**_Exit code_** of **_contained command_** otherwise
549
+
550
+    $ docker run busybox /bin/sh -c 'exit 3' 
551
+    # 3
552
+
521 553
 ## Clean up (--rm)
522 554
 
523 555
 By default a container's file system persists even after the container
... ...
@@ -599,15 +599,6 @@ var (
599 599
 		HTTPStatusCode: http.StatusInternalServerError,
600 600
 	})
601 601
 
602
-	// ErrorCodeCantStart is generated when an error occurred while
603
-	// trying to start a container.
604
-	ErrorCodeCantStart = errcode.Register(errGroup, errcode.ErrorDescriptor{
605
-		Value:          "CANTSTART",
606
-		Message:        "Cannot start container %s: %s",
607
-		Description:    "There was an error while trying to start a container",
608
-		HTTPStatusCode: http.StatusInternalServerError,
609
-	})
610
-
611 602
 	// ErrorCodeCantRestart is generated when an error occurred while
612 603
 	// trying to restart a container.
613 604
 	ErrorCodeCantRestart = errcode.Register(errGroup, errcode.ErrorDescriptor{
... ...
@@ -930,4 +921,31 @@ var (
930 930
 		Description:    "An attempt to create a volume using a driver but the volume already exists with a different driver",
931 931
 		HTTPStatusCode: http.StatusInternalServerError,
932 932
 	})
933
+
934
+	// ErrorCodeCmdNotFound is generated when contained cmd can't start,
935
+	// contained command not found error, exit code 127
936
+	ErrorCodeCmdNotFound = errcode.Register(errGroup, errcode.ErrorDescriptor{
937
+		Value:          "CMDNOTFOUND",
938
+		Message:        "Contained command not found or does not exist.",
939
+		Description:    "Command could not be found, command does not exist",
940
+		HTTPStatusCode: http.StatusInternalServerError,
941
+	})
942
+
943
+	// ErrorCodeCmdCouldNotBeInvoked is generated when contained cmd can't start,
944
+	// contained command permission denied error, exit code 126
945
+	ErrorCodeCmdCouldNotBeInvoked = errcode.Register(errGroup, errcode.ErrorDescriptor{
946
+		Value:          "CMDCOULDNOTBEINVOKED",
947
+		Message:        "Contained command could not be invoked.",
948
+		Description:    "Permission denied, cannot invoke command",
949
+		HTTPStatusCode: http.StatusInternalServerError,
950
+	})
951
+
952
+	// ErrorCodeCantStart is generated when contained cmd can't start,
953
+	// for any reason other than above 2 errors
954
+	ErrorCodeCantStart = errcode.Register(errGroup, errcode.ErrorDescriptor{
955
+		Value:          "CANTSTART",
956
+		Message:        "Cannot start container %s: %s",
957
+		Description:    "There was an error while trying to start a container",
958
+		HTTPStatusCode: http.StatusInternalServerError,
959
+	})
933 960
 )
... ...
@@ -1645,9 +1645,9 @@ func (s *DockerSuite) TestRunWorkdirExistsAndIsFile(c *check.C) {
1645 1645
 		expected = "The directory name is invalid"
1646 1646
 	}
1647 1647
 
1648
-	out, exit, err := dockerCmdWithError("run", "-w", existingFile, "busybox")
1649
-	if !(err != nil && exit == 1 && strings.Contains(out, expected)) {
1650
-		c.Fatalf("Docker must complains about making dir, but we got out: %s, exit: %d, err: %s", out, exit, err)
1648
+	out, exitCode, err := dockerCmdWithError("run", "-w", existingFile, "busybox")
1649
+	if !(err != nil && exitCode == 125 && strings.Contains(out, expected)) {
1650
+		c.Fatalf("Docker must complains about making dir with exitCode 125 but we got out: %s, exitCode: %d", out, exitCode)
1651 1651
 	}
1652 1652
 }
1653 1653
 
... ...
@@ -3746,17 +3746,72 @@ func (s *DockerSuite) TestRunStdinBlockedAfterContainerExit(c *check.C) {
3746 3746
 func (s *DockerSuite) TestRunWrongCpusetCpusFlagValue(c *check.C) {
3747 3747
 	// TODO Windows: This needs validation (error out) in the daemon.
3748 3748
 	testRequires(c, DaemonIsLinux)
3749
-	out, _, err := dockerCmdWithError("run", "--cpuset-cpus", "1-10,11--", "busybox", "true")
3749
+	out, exitCode, err := dockerCmdWithError("run", "--cpuset-cpus", "1-10,11--", "busybox", "true")
3750 3750
 	c.Assert(err, check.NotNil)
3751 3751
 	expected := "Error response from daemon: Invalid value 1-10,11-- for cpuset cpus.\n"
3752
-	c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
3752
+	if !(strings.Contains(out, expected) || exitCode == 125) {
3753
+		c.Fatalf("Expected output to contain %q with exitCode 125, got out: %q exitCode: %v", expected, out, exitCode)
3754
+	}
3753 3755
 }
3754 3756
 
3755 3757
 func (s *DockerSuite) TestRunWrongCpusetMemsFlagValue(c *check.C) {
3756 3758
 	// TODO Windows: This needs validation (error out) in the daemon.
3757 3759
 	testRequires(c, DaemonIsLinux)
3758
-	out, _, err := dockerCmdWithError("run", "--cpuset-mems", "1-42--", "busybox", "true")
3760
+	out, exitCode, err := dockerCmdWithError("run", "--cpuset-mems", "1-42--", "busybox", "true")
3759 3761
 	c.Assert(err, check.NotNil)
3760 3762
 	expected := "Error response from daemon: Invalid value 1-42-- for cpuset mems.\n"
3761
-	c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
3763
+	if !(strings.Contains(out, expected) || exitCode == 125) {
3764
+		c.Fatalf("Expected output to contain %q with exitCode 125, got out: %q exitCode: %v", expected, out, exitCode)
3765
+	}
3766
+}
3767
+
3768
+// TestRunNonExecutableCmd checks that 'docker run busybox foo' exits with error code 127'
3769
+func (s *DockerSuite) TestRunNonExecutableCmd(c *check.C) {
3770
+	name := "testNonExecutableCmd"
3771
+	runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "foo")
3772
+	_, exit, _ := runCommandWithOutput(runCmd)
3773
+	stateExitCode := findContainerExitCode(c, name)
3774
+	if !(exit == 127 && strings.Contains(stateExitCode, "127")) {
3775
+		c.Fatalf("Run non-executable command should have errored with exit code 127, but we got exit: %d, State.ExitCode: %s", exit, stateExitCode)
3776
+	}
3777
+}
3778
+
3779
+// TestRunNonExistingCmd checks that 'docker run busybox /bin/foo' exits with code 127.
3780
+func (s *DockerSuite) TestRunNonExistingCmd(c *check.C) {
3781
+	name := "testNonExistingCmd"
3782
+	runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "/bin/foo")
3783
+	_, exit, _ := runCommandWithOutput(runCmd)
3784
+	stateExitCode := findContainerExitCode(c, name)
3785
+	if !(exit == 127 && strings.Contains(stateExitCode, "127")) {
3786
+		c.Fatalf("Run non-existing command should have errored with exit code 127, but we got exit: %d, State.ExitCode: %s", exit, stateExitCode)
3787
+	}
3788
+}
3789
+
3790
+// TestCmdCannotBeInvoked checks that 'docker run busybox /etc' exits with 126.
3791
+func (s *DockerSuite) TestCmdCannotBeInvoked(c *check.C) {
3792
+	name := "testCmdCannotBeInvoked"
3793
+	runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "/etc")
3794
+	_, exit, _ := runCommandWithOutput(runCmd)
3795
+	stateExitCode := findContainerExitCode(c, name)
3796
+	if !(exit == 126 && strings.Contains(stateExitCode, "126")) {
3797
+		c.Fatalf("Run cmd that cannot be invoked should have errored with code 126, but we got exit: %d, State.ExitCode: %s", exit, stateExitCode)
3798
+	}
3799
+}
3800
+
3801
+// TestRunNonExistingImage checks that 'docker run foo' exits with error msg 125 and contains  'Unable to find image'
3802
+func (s *DockerSuite) TestRunNonExistingImage(c *check.C) {
3803
+	runCmd := exec.Command(dockerBinary, "run", "foo")
3804
+	out, exit, err := runCommandWithOutput(runCmd)
3805
+	if !(err != nil && exit == 125 && strings.Contains(out, "Unable to find image")) {
3806
+		c.Fatalf("Run non-existing image should have errored with 'Unable to find image' code 125, but we got out: %s, exit: %d, err: %s", out, exit, err)
3807
+	}
3808
+}
3809
+
3810
+// TestDockerFails checks that 'docker run -foo busybox' exits with 125 to signal docker run failed
3811
+func (s *DockerSuite) TestDockerFails(c *check.C) {
3812
+	runCmd := exec.Command(dockerBinary, "run", "-foo", "busybox")
3813
+	out, exit, err := runCommandWithOutput(runCmd)
3814
+	if !(err != nil && exit == 125) {
3815
+		c.Fatalf("Docker run with flag not defined should exit with 125, but we got out: %s, exit: %d, err: %s", out, exit, err)
3816
+	}
3762 3817
 }
... ...
@@ -397,8 +397,10 @@ func (s *DockerSuite) TestRunInvalidCpusetCpusFlagValue(c *check.C) {
397 397
 	}
398 398
 	out, _, err := dockerCmdWithError("run", "--cpuset-cpus", strconv.Itoa(invalid), "busybox", "true")
399 399
 	c.Assert(err, check.NotNil)
400
-	expected := fmt.Sprintf("Error response from daemon: Requested CPUs are not available - requested %s, available: %s.\n", strconv.Itoa(invalid), sysInfo.Cpus)
401
-	c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
400
+	expected := fmt.Sprintf("Error response from daemon: Requested CPUs are not available - requested %s, available: %s", strconv.Itoa(invalid), sysInfo.Cpus)
401
+	if !(strings.Contains(out, expected)) {
402
+		c.Fatalf("Expected output to contain %q, got %q", expected, out)
403
+	}
402 404
 }
403 405
 
404 406
 func (s *DockerSuite) TestRunInvalidCpusetMemsFlagValue(c *check.C) {
... ...
@@ -416,8 +418,10 @@ func (s *DockerSuite) TestRunInvalidCpusetMemsFlagValue(c *check.C) {
416 416
 	}
417 417
 	out, _, err := dockerCmdWithError("run", "--cpuset-mems", strconv.Itoa(invalid), "busybox", "true")
418 418
 	c.Assert(err, check.NotNil)
419
-	expected := fmt.Sprintf("Error response from daemon: Requested memory nodes are not available - requested %s, available: %s.\n", strconv.Itoa(invalid), sysInfo.Mems)
420
-	c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
419
+	expected := fmt.Sprintf("Error response from daemon: Requested memory nodes are not available - requested %s, available: %s", strconv.Itoa(invalid), sysInfo.Mems)
420
+	if !(strings.Contains(out, expected)) {
421
+		c.Fatalf("Expected output to contain %q, got %q", expected, out)
422
+	}
421 423
 }
422 424
 
423 425
 func (s *DockerSuite) TestRunInvalidCPUShares(c *check.C) {
... ...
@@ -129,11 +129,15 @@ func (s *DockerSuite) TestStartMultipleContainers(c *check.C) {
129 129
 
130 130
 	// start all the three containers, container `child_first` start first which should be failed
131 131
 	// container 'parent' start second and then start container 'child_second'
132
+	expOut := "Cannot link to a non running container"
133
+	expErr := "failed to start containers: [child_first]"
132 134
 	out, _, err = dockerCmdWithError("start", "child_first", "parent", "child_second")
133 135
 	// err shouldn't be nil because start will fail
134 136
 	c.Assert(err, checker.NotNil, check.Commentf("out: %s", out))
135 137
 	// output does not correspond to what was expected
136
-	c.Assert(out, checker.Contains, "Cannot start container child_first")
138
+	if !(strings.Contains(out, expOut) || strings.Contains(err.Error(), expErr)) {
139
+		c.Fatalf("Expected out: %v with err: %v  but got out: %v with err: %v", expOut, expErr, out, err)
140
+	}
137 141
 
138 142
 	for container, expected := range map[string]string{"parent": "true", "child_first": "false", "child_second": "true"} {
139 143
 		out, err := inspectField(container, "State.Running")
... ...
@@ -815,6 +815,17 @@ func dockerCmdInDirWithTimeout(timeout time.Duration, path string, args ...strin
815 815
 	return integration.DockerCmdInDirWithTimeout(dockerBinary, timeout, path, args...)
816 816
 }
817 817
 
818
+// find the State.ExitCode in container metadata
819
+func findContainerExitCode(c *check.C, name string, vargs ...string) string {
820
+	args := append(vargs, "inspect", "--format='{{ .State.ExitCode }} {{ .State.Error }}'", name)
821
+	cmd := exec.Command(dockerBinary, args...)
822
+	out, _, err := runCommandWithOutput(cmd)
823
+	if err != nil {
824
+		c.Fatal(err, out)
825
+	}
826
+	return out
827
+}
828
+
818 829
 func findContainerIP(c *check.C, id string, network string) string {
819 830
 	out, _ := dockerCmd(c, "inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.%s.IPAddress }}'", network), id)
820 831
 	return strings.Trim(out, " \r\n'")
... ...
@@ -508,6 +508,38 @@ running binaries within a container is the root directory (/). The developer can
508 508
 set a different default with the Dockerfile WORKDIR instruction. The operator
509 509
 can override the working directory by using the **-w** option.
510 510
 
511
+# Exit Status
512
+
513
+The exit code from `docker run` gives information about why the container
514
+failed to run or why it exited.  When `docker run` exits with a non-zero code,
515
+the exit codes follow the `chroot` standard, see below:
516
+
517
+**_125_** if the error is with Docker daemon **_itself_** 
518
+
519
+    $ docker run --foo busybox; echo $?
520
+    # flag provided but not defined: --foo
521
+      See 'docker run --help'.
522
+      125
523
+
524
+**_126_** if the **_contained command_** cannot be invoked
525
+
526
+    $ docker run busybox /etc; echo $?
527
+    # exec: "/etc": permission denied
528
+      docker: Error response from daemon: Contained command could not be invoked
529
+      126
530
+
531
+**_127_** if the **_contained command_** cannot be found
532
+
533
+    $ docker run busybox foo; echo $?
534
+    # exec: "foo": executable file not found in $PATH
535
+      docker: Error response from daemon: Contained command not found or does not exist
536
+      127
537
+
538
+**_Exit code_** of **_contained command_** otherwise 
539
+    
540
+    $ docker run busybox /bin/sh -c 'exit 3' 
541
+    # 3
542
+
511 543
 # EXAMPLES
512 544
 
513 545
 ## Exposing log messages from the container to the host's log
... ...
@@ -732,3 +764,4 @@ April 2014, Originally compiled by William Henry (whenry at redhat dot com)
732 732
 based on docker.com source material and internal work.
733 733
 June 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
734 734
 July 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
735
+November 2015, updated by Sally O'Malley <somalley@redhat.com>
... ...
@@ -1102,7 +1102,7 @@ func (fs *FlagSet) Parse(arguments []string) error {
1102 1102
 		case ContinueOnError:
1103 1103
 			return err
1104 1104
 		case ExitOnError:
1105
-			os.Exit(2)
1105
+			os.Exit(125)
1106 1106
 		case PanicOnError:
1107 1107
 			panic(err)
1108 1108
 		}