func Example_ThrottledReader() { totalSize := 10 * iocontrol.KiB readPerSec := 100 * iocontrol.KiB maxBurst := 10 * time.Millisecond input := randBytes(totalSize) src := bytes.NewReader(input) measured := iocontrol.NewMeasuredReader(src) throttled := iocontrol.ThrottledReader(measured, readPerSec, maxBurst) done := make(chan []byte) go func() { start := time.Now() output, err := ioutil.ReadAll(throttled) fmt.Printf("done in %.1fs", time.Since(start).Seconds()) if err != nil { log.Fatalf("error reading: %v", err) } done <- output }() for { select { case <-time.Tick(time.Millisecond * 10): log.Printf("reading at %s/s", humanize.IBytes(measured.BytesPerSec())) case output := <-done: if !bytes.Equal(input, output) { log.Print("==== input ====\n", hex.Dump(input)) log.Print("==== output ====\n", hex.Dump(output)) log.Fatalf("mismatch between input and output") } return } } // Output: // done in 0.1s }
// NewControlledProcess creates the child proc. func NewControlledProcess(cmd string, arguments []string, doneChan chan error, stdoutLimit int64) (JobControl, error) { var err error j := &Job{ nil, nil, 0, 0, doneChan, stdoutLimit, nil, } // Drop command from cmdline arguments and pass the rest as arguments separately var args []string if len(arguments) > 0 { args = arguments[1:] } j.Cmd = exec.Command(cmd, args...) // Collect stdout from the process to redirect to real stdout stdoutpipe, err := j.Cmd.StdoutPipe() if err != nil { return nil, fmt.Errorf("Failed to acquire stdout: %s", err) } stdout := iocontrol.NewMeasuredReader(stdoutpipe) j.stdoutReader = stdout var wg sync.WaitGroup stdin, err := j.Cmd.StdinPipe() if err != nil { return nil, fmt.Errorf("Failed to acquire stdin: %s", err) } stderr, err := j.Cmd.StderrPipe() if err != nil { return nil, fmt.Errorf("Failed to acquire stderr: %s", err) } // Map all child processes under this tree so Kill really ends everything. j.Cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, // Set process group ID } log.Debugf("%#v\n", j.Cmd) // Start the sub-process but don't wait for completion to pickup the Pid // for resource monitoring. err = j.Cmd.Start() if err != nil { return nil, fmt.Errorf("Failed to execute sub-process: %s\n", err) } j.Pid = j.Cmd.Process.Pid j.Pgid, err = syscall.Getpgid(j.Pid) if err != nil { return nil, fmt.Errorf("Failed syscall.Getpgid: %s\n", err) } j.Proc, err = process.NewProcess(int32(j.Pgid)) if err != nil { return nil, fmt.Errorf("Unable to create process.NewProcess: %s\n", err) } wg.Add(1) go func(wg *sync.WaitGroup, r io.Reader) { defer wg.Done() io.Copy(os.Stdout, r) log.Debugln("child closed stdout") }(&wg, stdout) go func(w io.WriteCloser) { io.Copy(w, os.Stdin) }(stdin) wg.Add(1) go func(wg *sync.WaitGroup, r io.Reader) { defer wg.Done() io.Copy(os.Stderr, r) log.Debugln("child closed stderr") }(&wg, stderr) // Background waiting for the job to finish and emit a done channel message // when complete. go func(wg *sync.WaitGroup, j *Job) { log.Debugln("Waiting on wg.Wait()") wg.Wait() log.Debugln("Waiting on Cmd.Wait()") err := j.Cmd.Wait() log.Debugf("Job finished: %q\n", err) j.done <- err }(&wg, j) return j, nil }