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  }