github.com/maruel/nin@v0.0.0-20220112143044-f35891e3ce7e/subprocess.go (about) 1 // Copyright 2012 Google Inc. All Rights Reserved. 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 // http://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 nin 16 17 import ( 18 "bytes" 19 "context" 20 "os" 21 "sync" 22 "sync/atomic" 23 ) 24 25 // The Go runtime already handles poll under the hood so this abstraction layer 26 // has to be replaced; unless we realize that the Go runtime is too slow. 27 28 // subprocess is the dumbest implementation, just to get going. 29 type subprocess struct { 30 done int32 31 exitCode int32 32 buf string 33 } 34 35 // Done queries if the process is done. 36 // 37 // Only used in tests. 38 func (s *subprocess) Done() bool { 39 return atomic.LoadInt32(&s.done) != 0 40 } 41 42 // Finish returns the exit code. Must only to be called after the process is 43 // done. 44 func (s *subprocess) Finish() ExitStatus { 45 return ExitStatus(s.exitCode) 46 } 47 48 func (s *subprocess) GetOutput() string { 49 return s.buf 50 } 51 52 func (s *subprocess) run(ctx context.Context, c string, useConsole bool) { 53 // The C++ code is fairly involved in its way to setup the process, the code 54 // here is fairly naive. 55 // TODO(maruel): Enable skipShell. This needs more testing. 56 cmd := createCmd(ctx, c, useConsole, false) 57 buf := bytes.Buffer{} 58 cmd.Stdout = &buf 59 cmd.Stderr = &buf 60 if useConsole { 61 cmd.Stdin = os.Stdin 62 } 63 _ = cmd.Run() 64 // Skip a memory copy. 65 s.buf = unsafeString(buf.Bytes()) 66 // TODO(maruel): For compatibility with ninja, use ExitInterrupted (2) for 67 // interrupted? 68 s.exitCode = int32(cmd.ProcessState.ExitCode()) 69 } 70 71 type subprocessSet struct { 72 ctx context.Context 73 cancel func() 74 wg sync.WaitGroup 75 procDone chan *subprocess 76 mu sync.Mutex 77 running []*subprocess 78 finished []*subprocess 79 } 80 81 func newSubprocessSet() *subprocessSet { 82 ctx, cancel := context.WithCancel(context.Background()) 83 return &subprocessSet{ 84 ctx: ctx, 85 cancel: cancel, 86 procDone: make(chan *subprocess), 87 } 88 } 89 90 // Clear interrupts all the children processes. 91 // 92 // TODO(maruel): Use a context instead. 93 func (s *subprocessSet) Clear() { 94 s.cancel() 95 s.wg.Wait() 96 // TODO(maruel): This is still broken, since the goroutines are stuck on 97 // s.procDone <- subproc. 98 } 99 100 // Running returns the number of running processes. 101 func (s *subprocessSet) Running() int { 102 s.mu.Lock() 103 r := len(s.running) 104 s.mu.Unlock() 105 return r 106 } 107 108 // Finished returns the number of processes to parse their output. 109 func (s *subprocessSet) Finished() int { 110 s.mu.Lock() 111 f := len(s.finished) 112 s.mu.Unlock() 113 return f 114 } 115 116 // Add starts a new child process. 117 func (s *subprocessSet) Add(c string, useConsole bool) *subprocess { 118 subproc := &subprocess{} 119 s.wg.Add(1) 120 go s.enqueue(subproc, c, useConsole) 121 s.mu.Lock() 122 s.running = append(s.running, subproc) 123 s.mu.Unlock() 124 return subproc 125 } 126 127 func (s *subprocessSet) enqueue(subproc *subprocess, c string, useConsole bool) { 128 subproc.run(s.ctx, c, useConsole) 129 // Do it before sending the channel because procDone is a blocking channel 130 // and the caller relies on Running() == 0 && Finished() == 0. Otherwise 131 // Clear() would hang. 132 s.wg.Done() 133 s.procDone <- subproc 134 } 135 136 // NextFinished returns the next finished child process. 137 func (s *subprocessSet) NextFinished() *subprocess { 138 s.mu.Lock() 139 var subproc *subprocess 140 if len(s.finished) != 0 { 141 // LIFO queue. 142 subproc = s.finished[len(s.finished)-1] 143 s.finished = s.finished[:len(s.finished)-1] 144 } 145 s.mu.Unlock() 146 return subproc 147 } 148 149 // DoWork should return on one of 3 events: 150 // 151 // - Was interrupted, return true 152 // - A process completed, return false 153 // - A pipe got data, returns false 154 // 155 // In Go, the later can't happen. 156 func (s *subprocessSet) DoWork() bool { 157 o := false 158 for { 159 select { 160 case p := <-s.procDone: 161 // TODO(maruel): Do a perf compare with a map[*Subprocess]struct{}. 162 s.mu.Lock() 163 i := 0 164 for i = range s.running { 165 if s.running[i] == p { 166 break 167 } 168 } 169 s.finished = append(s.finished, p) 170 if i < len(s.running)-1 { 171 copy(s.running[i:], s.running[i+1:]) 172 } 173 s.running = s.running[:len(s.running)-1] 174 s.mu.Unlock() 175 // The unit tests expect that Subprocess.Done() is only true once the 176 // subprocess has been added to finished. 177 atomic.StoreInt32(&p.done, 1) 178 default: 179 return o 180 } 181 } 182 }