github.com/Comcast/plax@v0.8.32/dsl/process.go (about)

     1  /*
     2   * Copyright 2021 Comcast Cable Communications Management, LLC
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   * http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   *
    16   * SPDX-License-Identifier: Apache-2.0
    17   */
    18  
    19  package dsl
    20  
    21  import (
    22  	"bufio"
    23  	"fmt"
    24  	"io"
    25  	"log"
    26  	"os/exec"
    27  	"strings"
    28  	"syscall"
    29  )
    30  
    31  // Process represents an external process run from a test.
    32  type Process struct {
    33  	// Name is an opaque string used is reports about this
    34  	// Process.
    35  	Name string `json:"name" yaml:"name"`
    36  
    37  	// Command is the name of the program.
    38  	//
    39  	// Subject to expansion.
    40  	Command string `json:"command" yaml:"command"`
    41  
    42  	// Args is the list of command-line arguments for the program.
    43  	//
    44  	// Subject to expansion.
    45  	Args []string `json:"args" yaml:"args"`
    46  
    47  	// ToDo: Environment, dir.
    48  
    49  	cmd *exec.Cmd
    50  
    51  	Stdout   chan string `json:"-"`
    52  	Stderr   chan string `json:"-"`
    53  	Stdin    chan string `json:"-"`
    54  	ExitCode chan int    `json:"-"`
    55  
    56  	// ctl is only used to terminate goroutines when the Process
    57  	// is terminated.
    58  	ctl chan bool
    59  }
    60  
    61  // Substitute the bindings into the Process
    62  func (p *Process) Substitute(ctx *Ctx, bs *Bindings) (*Process, error) {
    63  	cmd, err := bs.StringSub(ctx, p.Command)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	args := make([]string, len(p.Args))
    68  	for i, arg := range p.Args {
    69  		s, err := bs.StringSub(ctx, arg)
    70  		if err != nil {
    71  			return nil, err
    72  		}
    73  		args[i] = s
    74  	}
    75  	return &Process{
    76  		Name:    p.Name,
    77  		Command: cmd,
    78  		Args:    args,
    79  	}, nil
    80  }
    81  
    82  // TrimEOL is a utility function that removes the last (if any)
    83  // newline character(s).
    84  //
    85  // This function does not trim more than one newline.
    86  func TrimEOL(s string) string {
    87  	s = strings.TrimSuffix(s, "\n")
    88  	return strings.TrimSuffix(s, "\r")
    89  }
    90  
    91  // Start starts the program, which runs in the background (until the
    92  // test is complete).
    93  //
    94  // Stderr and stdout are logged via ctx.Logf.
    95  func (p *Process) Start(ctx *Ctx) error {
    96  
    97  	p.Stdin = make(chan string)
    98  	p.Stderr = make(chan string)
    99  	p.Stdout = make(chan string)
   100  	p.ctl = make(chan bool)
   101  	p.ExitCode = make(chan int)
   102  
   103  	p.cmd = exec.Command(p.Command, p.Args...)
   104  
   105  	inPipe, err := p.cmd.StdinPipe()
   106  	if err != nil {
   107  		return fmt.Errorf("Process %s Run error on StdinPipe: %s", p.Name, err)
   108  	}
   109  
   110  	go func() {
   111  		for {
   112  			select {
   113  			case <-ctx.Done():
   114  				return
   115  			case line := <-p.Stdin:
   116  				line = TrimEOL(line) + "\n"
   117  				if _, err = io.WriteString(inPipe, line); err != nil {
   118  					log.Printf("Warning: Process Write: %s", err)
   119  				}
   120  			}
   121  		}
   122  	}()
   123  
   124  	errPipe, err := p.cmd.StderrPipe()
   125  	if err != nil {
   126  		return fmt.Errorf("Process %s Run error on StderrPipe: %s", p.Name, err)
   127  	}
   128  
   129  	go func() {
   130  		in := bufio.NewScanner(errPipe)
   131  		for in.Scan() {
   132  			line := in.Text()
   133  			ctx.Logf("Process %s stderr line: %s\n", p.Name, line)
   134  			select {
   135  			case <-ctx.Done():
   136  				return
   137  			case <-p.ctl:
   138  				return
   139  			case p.Stderr <- line:
   140  			}
   141  		}
   142  		if err := in.Err(); err != nil {
   143  			ctx.Logf("Process %s stderr error %s", p.Name, err)
   144  		}
   145  	}()
   146  
   147  	outPipe, err := p.cmd.StdoutPipe()
   148  	if err != nil {
   149  		return fmt.Errorf("Process %s Run error on StdoutPipe: %s", p.Name, err)
   150  	}
   151  
   152  	go func() {
   153  		in := bufio.NewScanner(outPipe)
   154  		for in.Scan() {
   155  			line := in.Text()
   156  			ctx.Logf("Process %s stdout line: %s\n", p.Name, line)
   157  			select {
   158  			case <-ctx.Done():
   159  				return
   160  			case <-p.ctl:
   161  				return
   162  			case p.Stdout <- line:
   163  			}
   164  		}
   165  		if err := in.Err(); err != nil {
   166  			ctx.Logf("Process %s stdout error %s", p.Name, err)
   167  		}
   168  	}()
   169  
   170  	if err := p.cmd.Start(); err != nil {
   171  		ctx.Logf("Process %s error on start: %s", p.Name, err)
   172  		return err
   173  	}
   174  
   175  	go func() {
   176  		if err := p.cmd.Wait(); err != nil {
   177  			ctx.Logf("Process %s error on wait: %s", p.Name, err)
   178  		}
   179  
   180  		p.ExitCode <- p.cmd.ProcessState.ExitCode()
   181  	}()
   182  
   183  	return nil
   184  }
   185  
   186  // Term sends a SIGTERM to the process.
   187  func (p *Process) Term(ctx *Ctx) error {
   188  	close(p.ctl)
   189  	p.cmd.Process.Signal(syscall.SIGTERM)
   190  	return nil
   191  }
   192  
   193  // Kill kills the Process.
   194  func (p *Process) Kill(ctx *Ctx) error {
   195  	ctx.Logf("Process %s killed", p.Name)
   196  	return p.cmd.Process.Kill()
   197  }