mirrored from https://chromium.googlesource.com/infra/luci/luci-go
-
Notifications
You must be signed in to change notification settings - Fork 43
/
subprocess.go
142 lines (125 loc) · 4.31 KB
/
subprocess.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Copyright 2019 The LUCI Authors.
//
// 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 invoke
import (
"bytes"
"context"
"os/exec"
"sync"
"github.com/golang/protobuf/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/luciexe"
bbpb "go.chromium.org/luci/buildbucket/proto"
)
// Subprocess represents a running luciexe.
type Subprocess struct {
Step *bbpb.Step
collectPath string
cmd *exec.Cmd
closeChannels chan<- struct{}
allClosed <-chan error
waitOnce sync.Once
build *bbpb.Build
err error
}
// Start launches a binary implementing the luciexe protocol and returns
// immediately with a *Subprocess.
//
// Args:
// * ctx will be used for deadlines/cancellation of the started luciexe.
// * luciexeArgs[0] must be the full absolute path to the luciexe binary.
// * input must be the Build message you wish to pass to the luciexe binary.
// * opts is optional (may be nil to take all defaults)
//
// Callers MUST call Wait and/or cancel the context or this will leak handles
// for the process' stdout/stderr.
//
// This assumes that the current process is already operating within a "host
// application" environment. See "go.chromium.org/luci/luciexe" for details.
//
// The caller SHOULD immediately take Subprocess.Step, append it to the current
// Build state, and send that (e.g. using `exe.BuildSender`). Otherwise this
// luciexe's steps will not show up in the Build.
func Start(ctx context.Context, luciexeArgs []string, input *bbpb.Build, opts *Options) (*Subprocess, error) {
inputData, err := proto.Marshal(input)
if err != nil {
return nil, errors.Annotate(err, "marshalling input Build").Err()
}
launchOpts, _, err := opts.rationalize(ctx)
if err != nil {
return nil, errors.Annotate(err, "normalizing options").Err()
}
closeChannels := make(chan struct{})
allClosed := make(chan error)
go func() {
select {
case <-ctx.Done():
case <-closeChannels:
}
err := errors.NewLazyMultiError(2)
err.Assign(0, errors.Annotate(launchOpts.stdout.Close(), "closing stdout").Err())
err.Assign(1, errors.Annotate(launchOpts.stderr.Close(), "closing stderr").Err())
allClosed <- err.Get()
}()
args := make([]string, 0, len(luciexeArgs)+len(launchOpts.args)-1)
args = append(args, luciexeArgs[1:]...)
args = append(args, launchOpts.args...)
cmd := exec.CommandContext(ctx, luciexeArgs[0], args...)
cmd.Env = launchOpts.env.Sorted()
cmd.Dir = launchOpts.workDir
cmd.Stdin = bytes.NewBuffer(inputData)
cmd.Stdout = launchOpts.stdout
cmd.Stderr = launchOpts.stderr
if err := cmd.Start(); err != nil {
// clean up stdout/stderr
close(closeChannels)
<-allClosed
return nil, errors.Annotate(err, "launching luciexe").Err()
}
return &Subprocess{
Step: launchOpts.step,
collectPath: launchOpts.collectPath,
cmd: cmd,
closeChannels: closeChannels,
allClosed: allClosed,
}, nil
}
// Wait waits for the subprocess to terminate.
//
// If Options.CollectOutput (default: false) was specified, this will return the
// final Build message, as reported by the luciexe.
//
// If you wish to cancel the subprocess (e.g. due to a timeout or deadline),
// make sure to pass a cancelable/deadline context to Start().
//
// Calling this multiple times is OK; it will return the same values every time.
func (s *Subprocess) Wait() (*bbpb.Build, error) {
s.waitOnce.Do(func() {
// No matter what, we want to close stdout/stderr; if none of the other
// return values have set `err`, it will be set to the result of closing
// stdout/stderr.
defer func() {
close(s.closeChannels)
if closeErr := <-s.allClosed; s.err == nil {
s.err = closeErr
}
}()
if s.err = s.cmd.Wait(); s.err != nil {
s.err = errors.Annotate(s.err, "waiting for luciexe").Err()
return
}
s.build, s.err = luciexe.ReadBuildFile(s.collectPath)
})
return s.build, s.err
}