diff --git a/cmd/run_with_timeout/Android.bp b/cmd/run_with_timeout/Android.bp new file mode 100644 index 000000000..76262cce7 --- /dev/null +++ b/cmd/run_with_timeout/Android.bp @@ -0,0 +1,27 @@ +// Copyright 2021 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +blueprint_go_binary { + name: "run_with_timeout", + srcs: [ + "run_with_timeout.go", + ], + testSrcs: [ + "run_with_timeout_test.go", + ], +} diff --git a/cmd/run_with_timeout/run_with_timeout.go b/cmd/run_with_timeout/run_with_timeout.go new file mode 100644 index 000000000..e2258726c --- /dev/null +++ b/cmd/run_with_timeout/run_with_timeout.go @@ -0,0 +1,110 @@ +// Copyright 2021 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// run_with_timeout is a utility that can kill a wrapped command after a configurable timeout, +// optionally running a command to collect debugging information first. + +package main + +import ( + "flag" + "fmt" + "io" + "os" + "os/exec" + "syscall" + "time" +) + +var ( + timeout = flag.Duration("timeout", 0, "time after which to kill command (example: 60s)") + onTimeoutCmd = flag.String("on_timeout", "", "command to run with `PID= sh -c` after timeout.") +) + +func usage() { + fmt.Fprintf(os.Stderr, "usage: %s [--timeout N] [--on_timeout CMD] -- command [args...]\n", os.Args[0]) + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, "run_with_timeout is a utility that can kill a wrapped command after a configurable timeout,") + fmt.Fprintln(os.Stderr, "optionally running a command to collect debugging information first.") + + os.Exit(2) +} + +func main() { + flag.Usage = usage + flag.Parse() + + if flag.NArg() < 1 { + fmt.Fprintln(os.Stderr, "command is required") + usage() + } + + err := runWithTimeout(flag.Arg(0), flag.Args()[1:], *timeout, *onTimeoutCmd, + os.Stdin, os.Stdout, os.Stderr) + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + fmt.Fprintln(os.Stderr, "process exited with error:", exitErr.Error()) + } else { + fmt.Fprintln(os.Stderr, "error:", err.Error()) + } + os.Exit(1) + } +} + +func runWithTimeout(command string, args []string, timeout time.Duration, onTimeoutCmdStr string, + stdin io.Reader, stdout, stderr io.Writer) error { + cmd := exec.Command(command, args...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = stdin, stdout, stderr + err := cmd.Start() + if err != nil { + return err + } + + // waitCh will signal the subprocess exited. + waitCh := make(chan error) + go func() { + waitCh <- cmd.Wait() + }() + + // timeoutCh will signal the subprocess timed out if timeout was set. + var timeoutCh <-chan time.Time = make(chan time.Time) + if timeout > 0 { + timeoutCh = time.After(timeout) + } + + select { + case err := <-waitCh: + if exitErr, ok := err.(*exec.ExitError); ok { + return fmt.Errorf("process exited with error: %w", exitErr) + } + return err + case <-timeoutCh: + // Continue below. + } + + // Process timed out before exiting. + defer cmd.Process.Signal(syscall.SIGKILL) + + if onTimeoutCmdStr != "" { + onTimeoutCmd := exec.Command("sh", "-c", onTimeoutCmdStr) + onTimeoutCmd.Stdin, onTimeoutCmd.Stdout, onTimeoutCmd.Stderr = stdin, stdout, stderr + onTimeoutCmd.Env = append(os.Environ(), fmt.Sprintf("PID=%d", cmd.Process.Pid)) + err := onTimeoutCmd.Run() + if err != nil { + return fmt.Errorf("on_timeout command %q exited with error: %w", onTimeoutCmdStr, err) + } + } + + return fmt.Errorf("timed out after %s", timeout.String()) +} diff --git a/cmd/run_with_timeout/run_with_timeout_test.go b/cmd/run_with_timeout/run_with_timeout_test.go new file mode 100644 index 000000000..aebd33600 --- /dev/null +++ b/cmd/run_with_timeout/run_with_timeout_test.go @@ -0,0 +1,94 @@ +// Copyright 2021 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "io" + "testing" + "time" +) + +func Test_runWithTimeout(t *testing.T) { + type args struct { + command string + args []string + timeout time.Duration + onTimeoutCmd string + stdin io.Reader + } + tests := []struct { + name string + args args + wantStdout string + wantStderr string + wantErr bool + }{ + { + name: "no timeout", + args: args{ + command: "echo", + args: []string{"foo"}, + }, + wantStdout: "foo\n", + }, + { + name: "timeout not reached", + args: args{ + command: "echo", + args: []string{"foo"}, + timeout: 1 * time.Second, + }, + wantStdout: "foo\n", + }, + { + name: "timed out", + args: args{ + command: "sh", + args: []string{"-c", "sleep 1 && echo foo"}, + timeout: 1 * time.Millisecond, + }, + wantErr: true, + }, + { + name: "on_timeout command", + args: args{ + command: "sh", + args: []string{"-c", "sleep 1 && echo foo"}, + timeout: 1 * time.Millisecond, + onTimeoutCmd: "echo bar", + }, + wantStdout: "bar\n", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + err := runWithTimeout(tt.args.command, tt.args.args, tt.args.timeout, tt.args.onTimeoutCmd, tt.args.stdin, stdout, stderr) + if (err != nil) != tt.wantErr { + t.Errorf("runWithTimeout() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotStdout := stdout.String(); gotStdout != tt.wantStdout { + t.Errorf("runWithTimeout() gotStdout = %v, want %v", gotStdout, tt.wantStdout) + } + if gotStderr := stderr.String(); gotStderr != tt.wantStderr { + t.Errorf("runWithTimeout() gotStderr = %v, want %v", gotStderr, tt.wantStderr) + } + }) + } +} diff --git a/java/config/config.go b/java/config/config.go index 30c6f91aa..273084c85 100644 --- a/java/config/config.go +++ b/java/config/config.go @@ -69,6 +69,8 @@ func init() { pctx.StaticVariable("JavacHeapSize", "2048M") pctx.StaticVariable("JavacHeapFlags", "-J-Xmx${JavacHeapSize}") pctx.StaticVariable("DexFlags", "-JXX:OnError='cat hs_err_pid%p.log' -JXX:CICompilerCount=6 -JXX:+UseDynamicNumberOfGCThreads") + // TODO(b/181095653): remove duplicated flags. + pctx.StaticVariable("DexJavaFlags", "-XX:OnError='cat hs_err_pid%p.log' -XX:CICompilerCount=6 -XX:+UseDynamicNumberOfGCThreads -Xmx2G") pctx.StaticVariable("CommonJdkFlags", strings.Join([]string{ `-Xmaxerrs 9999999`, diff --git a/java/dex.go b/java/dex.go index 7898e9dff..6bf0143b1 100644 --- a/java/dex.go +++ b/java/dex.go @@ -84,6 +84,11 @@ func (d *dexer) effectiveOptimizeEnabled() bool { return BoolDefault(d.dexProperties.Optimize.Enabled, d.dexProperties.Optimize.EnabledByDefault) } +func init() { + pctx.HostBinToolVariable("runWithTimeoutCmd", "run_with_timeout") + pctx.SourcePathVariable("jstackCmd", "${config.JavaToolchain}/jstack") +} + var d8, d8RE = pctx.MultiCommandRemoteStaticRules("d8", blueprint.RuleParams{ Command: `rm -rf "$outDir" && mkdir -p "$outDir" && ` + @@ -117,7 +122,10 @@ var r8, r8RE = pctx.MultiCommandRemoteStaticRules("r8", Command: `rm -rf "$outDir" && mkdir -p "$outDir" && ` + `rm -f "$outDict" && rm -rf "${outUsageDir}" && ` + `mkdir -p $$(dirname ${outUsage}) && ` + - `$r8Template${config.R8Cmd} ${config.DexFlags} -injars $in --output $outDir ` + + // TODO(b/181095653): remove R8 timeout and go back to config.R8Cmd. + `${runWithTimeoutCmd} -timeout 30m -on_timeout '${jstackCmd} $$PID' -- ` + + `$r8Template${config.JavaCmd} ${config.DexJavaFlags} -cp ${config.R8Jar} ` + + `com.android.tools.r8.compatproguard.CompatProguard -injars $in --output $outDir ` + `--no-data-resources ` + `-printmapping ${outDict} ` + `-printusage ${outUsage} ` + @@ -128,9 +136,10 @@ var r8, r8RE = pctx.MultiCommandRemoteStaticRules("r8", `$zipTemplate${config.SoongZipCmd} $zipFlags -o $outDir/classes.dex.jar -C $outDir -f "$outDir/classes*.dex" && ` + `${config.MergeZipsCmd} -D -stripFile "**/*.class" $out $outDir/classes.dex.jar $in`, CommandDeps: []string{ - "${config.R8Cmd}", + "${config.R8Jar}", "${config.SoongZipCmd}", "${config.MergeZipsCmd}", + "${runWithTimeoutCmd}", }, }, map[string]*remoteexec.REParams{ "$r8Template": &remoteexec.REParams{