diff --git a/.travis.yml b/.travis.yml index 809f0501..8ec7f455 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - 1.13.x - 1.14.x + - 1.15.x - tip os: @@ -19,7 +19,7 @@ matrix: - go: tip env: - - SHELLCHECK_VERSION=0.7.1 HADOLINT_VERSION=1.17.6 + - SHELLCHECK_VERSION=0.7.1 HADOLINT_VERSION=1.18.0 before_install: - wget https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz diff --git a/COOKBOOK.md b/COOKBOOK.md index f82f66c9..d33d6071 100644 --- a/COOKBOOK.md +++ b/COOKBOOK.md @@ -8,6 +8,7 @@ * [`require-root`](#require-root) * [`fast-finish`](#fast-finish) * [`lock-workdir`](#lock-workdir) + * [`unbuffer`](#unbuffer) * [`delay`](#delay) * [`command`](#command) * [Variables](#variables) @@ -17,6 +18,8 @@ * [`wait`](#wait) * [Input/Output](#inputoutput) * [`expect`](#expect) + * [`expect-stdout`](#expect-stdout) + * [`expect-stderr`](#expect-stderr) * [`print`](#print) * [`wait-output`](#wait-output) * [`output-match`](#output-match) @@ -97,7 +100,7 @@ dir "/home/john" ``` -
+ ### Global @@ -118,7 +121,7 @@ pkg php nginx libhttp2 libhttp2-devel ``` -
+ #### `unsafe-actions` @@ -137,7 +140,7 @@ unsafe-actions yes ``` -
+ #### `require-root` @@ -158,7 +161,7 @@ require-root yes ``` -
+ #### `fast-finish` @@ -177,7 +180,7 @@ fast-finish yes ``` -
+ #### `lock-workdir` @@ -196,7 +199,26 @@ lock-workdir no ``` -
+ + +#### `unbuffer` + +Disables I/O stream buffering. + +**Syntax:** `unbuffer ` + +**Arguments:** + +* `flag` - Flag (_Boolean_) [`no` by default] + +**Example:** + +```yang +unbuffer yes + +``` + + #### `delay` @@ -215,7 +237,7 @@ delay 1.5 ``` -
+ #### `command` @@ -262,7 +284,7 @@ command:init "my app initdb" "Init database" ``` -
+ ### Variables @@ -302,7 +324,7 @@ command "service start {service}" "Starting service" ``` -
+ ### Actions @@ -339,7 +361,7 @@ command "git clone git@github.com:user/repo.git" "Repository clone" ``` -
+ ##### `wait` @@ -361,7 +383,7 @@ command "echo 'ABCD'" "Simple echo command" ``` -
+ #### Input/Output @@ -396,7 +418,53 @@ command "echo 'ABCD'" "Simple echo command with 1 seconds timeout" ``` -
+ + +##### `expect-stdout` + +Expects some substring in command [standard output](http://www.linfo.org/standard_output.html). + +**Syntax:** `expect-stdout [max-wait]` + +**Arguments:** + +* `substr` - Substring for search (_String_) +* `max-wait` - Max wait time in seconds (_Float_) [Optional | 5 seconds] + +**Negative form:** No + +**Example:** + +```yang +command "myApp 1" "Simple command" + expect-stdout "Everything fine" + +``` + + + +##### `expect-stderr` + +Expects some substring in command [standard error](http://www.linfo.org/standard_error.html). + +**Syntax:** `expect-stderr [max-wait]` + +**Arguments:** + +* `substr` - Substring for search (_String_) +* `max-wait` - Max wait time in seconds (_Float_) [Optional | 5 seconds] + +**Negative form:** No + +**Example:** + +```yang +command "myApp ABCD" "Simple command" + expect-stderr "Error!" + +``` + + ##### `print` @@ -418,7 +486,7 @@ command "echo 'ABCD'" "Simple echo command" ``` -
+ ##### `wait-output` @@ -460,7 +528,7 @@ command "echo 'ABCD'" "Simple echo command" ``` -
+ ##### `output-contains` @@ -482,7 +550,7 @@ command "echo 'ABCD'" "Simple echo command" ``` -
+ ##### `output-trim` @@ -500,7 +568,7 @@ command "echo 'ABCD'" "Simple echo command" ``` -
+ #### Filesystem @@ -527,7 +595,7 @@ command "-" "Check environment" ``` -
+ ##### `mode` @@ -550,7 +618,7 @@ command "-" "Check environment" ``` -
+ ##### `owner` @@ -576,7 +644,7 @@ command "-" "Check environment" ``` -
+ ##### `exist` @@ -598,7 +666,7 @@ command "-" "Check environment" ``` -
+ ##### `readable` @@ -621,7 +689,7 @@ command "-" "Check environment" ``` -
+ ##### `writable` @@ -644,7 +712,7 @@ command "-" "Check environment" ``` -
+ ##### `executable` @@ -667,7 +735,7 @@ command "-" "Check environment" ``` -
+ ##### `dir` @@ -689,7 +757,7 @@ command "-" "Check environment" ``` -
+ ##### `empty` @@ -711,7 +779,7 @@ command "-" "Check environment" ``` -
+ ##### `empty-dir` @@ -733,7 +801,7 @@ command "-" "Check environment" ``` -
+ ##### `checksum` @@ -756,7 +824,7 @@ command "-" "Check environment" ``` -
+ ##### `checksum-read` @@ -779,7 +847,7 @@ command "-" "Check environment" ``` -
+ ##### `file-contains` @@ -802,7 +870,7 @@ command "-" "Check environment" ``` -
+ ##### `copy` @@ -825,7 +893,7 @@ command "-" "Check environment" ``` -
+ ##### `move` @@ -848,7 +916,7 @@ command "-" "Check environment" ``` -
+ ##### `touch` @@ -870,7 +938,7 @@ command "-" "Check environment" ``` -
+ ##### `mkdir` @@ -892,7 +960,7 @@ command "-" "Check environment" ``` -
+ ##### `remove` @@ -916,7 +984,7 @@ command "-" "Check environment" ``` -
+ ##### `chmod` @@ -939,7 +1007,7 @@ command "-" "Check environment" ``` -
+ ##### `backup` @@ -961,7 +1029,7 @@ command "-" "Configure environment" ``` -
+ ##### `backup-restore` @@ -984,7 +1052,7 @@ command "-" "Configure environment" ``` -
+ #### System @@ -1008,7 +1076,7 @@ command "-" "Check environment" ``` -
+ ##### `wait-pid` @@ -1037,7 +1105,7 @@ command "-" "Check environment" ``` -
+ ##### `wait-fs` @@ -1066,7 +1134,7 @@ command "service myapp start" "Starting MyApp" ``` -
+ ##### `connect` @@ -1091,7 +1159,7 @@ command "-" "Check environment" ``` -
+ ##### `app` @@ -1113,7 +1181,7 @@ command "-" "Check environment" ``` -
+ ##### `signal` @@ -1150,7 +1218,7 @@ command "myapp --daemon" "Check my app" ``` -
+ ##### `env` @@ -1173,7 +1241,7 @@ command "-" "Check environment" ``` -
+ ##### `env-set` @@ -1196,7 +1264,7 @@ command "-" "Prepare environment" ``` -
+ #### Users/Groups @@ -1220,7 +1288,7 @@ command "-" "Check environment" ``` -
+ ##### `user-id` @@ -1243,7 +1311,7 @@ command "-" "Check environment" ``` -
+ ##### `user-gid` @@ -1266,7 +1334,7 @@ command "-" "Check environment" ``` -
+ ##### `user-group` @@ -1289,7 +1357,7 @@ command "-" "Check environment" ``` -
+ ##### `user-shell` @@ -1312,7 +1380,7 @@ command "-" "Check environment" ``` -
+ ##### `user-home` @@ -1335,7 +1403,7 @@ command "-" "Check environment" ``` -
+ ##### `group-exist` @@ -1357,7 +1425,7 @@ command "-" "Check environment" ``` -
+ ##### `group-id` @@ -1380,7 +1448,7 @@ command "-" "Check environment" ``` -
+ #### Services @@ -1404,7 +1472,7 @@ command "-" "Check environment" ``` -
+ ##### `service-enabled` @@ -1426,7 +1494,7 @@ command "-" "Check environment" ``` -
+ ##### `service-works` @@ -1448,7 +1516,7 @@ command "-" "Check environment" ``` -
+ #### HTTP @@ -1481,7 +1549,7 @@ command "-" "Make HTTP request" ``` -
+ ##### `http-header` @@ -1513,7 +1581,7 @@ command "-" "Make HTTP request" ``` -
+ ##### `http-contains` @@ -1538,7 +1606,7 @@ command "-" "Make HTTP request" ``` -
+ ##### `http-json` @@ -1563,7 +1631,7 @@ command "-" "Make HTTP request and check domain info" ``` -
+ ##### `http-set-auth` @@ -1592,7 +1660,7 @@ command "-" "Make HTTP request without auth" ``` -
+ ##### `http-set-header` @@ -1618,7 +1686,7 @@ command "-" "Make HTTP request" ``` -
+ #### Libraries @@ -1642,7 +1710,7 @@ command "-" "Check environment" ``` -
+ ##### `lib-header` @@ -1664,7 +1732,7 @@ command "-" "Check environment" ``` -
+ ##### `lib-config` @@ -1686,7 +1754,7 @@ command "-" "Check environment" ``` -
+ ##### `lib-exist` @@ -1709,7 +1777,7 @@ command "-" "Check environment" ``` -
+ ##### `lib-linked` @@ -1732,7 +1800,7 @@ command "-" "Check environment" ``` -
+ #### Python @@ -1756,7 +1824,7 @@ command "-" "Check Python module loading" ``` -
+ ##### `python3-module` @@ -1778,54 +1846,58 @@ command "-" "Check Python 3 module loading" ``` -
+ ## Examples ```yang -# Simple recipe for mkcryptpasswd utility +# Bibop recipe for MkCryptPasswd -command "mkcryptpasswd" "Generate basic hash for password" - expect "Please enter password" - print "MyPassword1234" +pkg mkcryptpasswd + +fast-finish yes + +var password MyPassword1234 +var salt SALT1234 +var salt_length 9 + +command "mkcryptpasswd -s" "Generate basic hash for password" + expect "Please enter password:" + print "{password}" expect "Hash: " exit 0 -command "mkcryptpasswd -sa SALT1234" "Generate hash for password with predefined salt" +command "mkcryptpasswd -s -sa {salt}" "Generate hash for password with predefined salt" expect "Please enter password" - print "MyPassword1234" - wait 1 - output-contains "$6$SALT1234$lTxNu4.6r/j81sirgJ.s9ai8AA3tJdp67XBWLFiE10tIharVYtzRJ9eJ9YEtQsiLzVtg94GrXAYjf40pWEEg7/" + print "{password}" + expect "$6${salt}$lTxNu4.6r/j81sirgJ.s9ai8AA3tJdp67XBWLFiE10tIharVYtzRJ9eJ9YEtQsiLzVtg94GrXAYjf40pWEEg7/" exit 0 -command "mkcryptpasswd -sa SALT1234 -1" "Generate MD5 hash for password with predefined salt" +command "mkcryptpasswd -s -sa {salt} -1" "Generate MD5 hash for password with predefined salt" expect "Please enter password" - print "MyPassword1234" - wait 1 - output-contains "$1$SALT1234$zIPLJYODoLlesdP3bf95S1" + print "{password}" + expect "$1${salt}$zIPLJYODoLlesdP3bf95S1" exit 0 -command "mkcryptpasswd -sa SALT1234 -5" "Generate SHA256 hash for password with predefined salt" +command "mkcryptpasswd -s -sa {salt} -5" "Generate SHA256 hash for password with predefined salt" expect "Please enter password" - print "MyPassword1234" - wait 1 - output-contains "$5$SALT1234$HOV.9Dkp4HSDzcfizNDG7x5ST4e74zcezvCJ8BWHuK8" + print "{password}" + expect "$5${salt}$HOV.9Dkp4HSDzcfizNDG7x5ST4e74zcezvCJ8BWHuK8" exit 0 -command "mkcryptpasswd -S" "Return error if password is too weak" +command "mkcryptpasswd -s -S" "Return error if password is too weak" expect "Please enter password" print "password" expect "Password is too weak: it is based on a dictionary word" print "password" - wait 0.5 + expect "Password is too weak: it is based on a dictionary word" print "password" - wait 0.5 - exit 1 + expect "Password is too weak: it is based on a dictionary word" + !exit 0 command "mkcryptpasswd --abcd" "Return error about unsupported argument" expect "Error! You used unsupported argument --abcd. Please check command syntax." - exit 1 - + !exit 0 ``` diff --git a/README.md b/README.md index 853ff3c9..bd9ded62 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,15 @@ Usage: bibop {options} recipe Options + --dry-run, -D Parse and validate recipe + --list-packages, -L List required packages --dir, -d dir Path to working directory + --path, -p path Path to directory with binaries --error-dir, -e dir Path to directory for errors data --tag, -t tag Command tag --quiet, -q Quiet mode - --ignore-packages, -ip Skip packages check - --dry-run, -D Parse and validate recipe - --list-packages, -L List required packages + --ignore-packages, -ip Do not check system for installed packages + --no-cleanup, -nl Disable deleting files created during tests --no-color, -nc Disable colors in output --help, -h Show this help message --version, -v Show version diff --git a/action/io.go b/action/io.go index 61e61667..3934d5fc 100644 --- a/action/io.go +++ b/action/io.go @@ -56,7 +56,7 @@ func Expect(action *recipe.Action, outputStore *output.Store) error { for range time.NewTicker(_DATA_READ_PERIOD).C { if bytes.Contains(stdout.Bytes(), []byte(substr)) || bytes.Contains(stderr.Bytes(), []byte(substr)) { - outputStore.Clear = true + outputStore.Purge() return nil } @@ -65,7 +65,87 @@ func Expect(action *recipe.Action, outputStore *output.Store) error { } } - outputStore.Clear = true + outputStore.Purge() + + return fmt.Errorf("Timeout (%g sec) reached", timeout) +} + +// ExpectStdout is action processor for "expect-stdout" +func ExpectStdout(action *recipe.Action, outputStore *output.Store) error { + var timeout float64 + + substr, err := action.GetS(0) + + if err != nil { + return err + } + + if action.Has(1) { + timeout, err = action.GetF(1) + + if err != nil { + return err + } + } else { + timeout = 5.0 + } + + start := time.Now() + timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0) + timeoutDur := secondsToDuration(timeout) + + for range time.NewTicker(_DATA_READ_PERIOD).C { + if bytes.Contains(outputStore.Stderr.Bytes(), []byte(substr)) { + outputStore.Stderr.Purge() + return nil + } + + if time.Since(start) >= timeoutDur { + break + } + } + + outputStore.Stderr.Purge() + + return fmt.Errorf("Timeout (%g sec) reached", timeout) +} + +// ExpectStderr is action processor for "expect-stderr" +func ExpectStderr(action *recipe.Action, outputStore *output.Store) error { + var timeout float64 + + substr, err := action.GetS(0) + + if err != nil { + return err + } + + if action.Has(1) { + timeout, err = action.GetF(1) + + if err != nil { + return err + } + } else { + timeout = 5.0 + } + + start := time.Now() + timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0) + timeoutDur := secondsToDuration(timeout) + + for range time.NewTicker(_DATA_READ_PERIOD).C { + if bytes.Contains(outputStore.Stdout.Bytes(), []byte(substr)) { + outputStore.Stdout.Purge() + return nil + } + + if time.Since(start) >= timeoutDur { + break + } + } + + outputStore.Stdout.Purge() return fmt.Errorf("Timeout (%g sec) reached", timeout) } @@ -106,9 +186,9 @@ func Input(action *recipe.Action, input io.Writer, outputStore *output.Store) er text = text + "\n" } - _, err = input.Write([]byte(text)) + outputStore.Purge() - outputStore.Clear = true + _, err = input.Write([]byte(text)) return err } @@ -159,6 +239,6 @@ func OutputContains(action *recipe.Action, outputStore *output.Store) error { // OutputTrim is action processor for "output-trim" func OutputTrim(action *recipe.Action, outputStore *output.Store) error { - outputStore.Clear = true + outputStore.Purge() return nil } diff --git a/cli/cli.go b/cli/cli.go index 687a057c..ea278ca4 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -34,7 +34,7 @@ import ( // Application info const ( APP = "bibop" - VER = "2.4.0" + VER = "2.5.0" DESC = "Utility for testing command-line tools" ) @@ -42,16 +42,18 @@ const ( // Options const ( - OPT_DIR = "d:dir" - OPT_ERROR_DIR = "e:error-dir" - OPT_TAG = "t:tag" - OPT_QUIET = "q:quiet" - OPT_DRY_RUN = "D:dry-run" - OPT_LIST_PACKAGES = "L:list-packages" - OPT_NO_CLEANUP = "NC:no-cleanup" - OPT_NO_COLOR = "nc:no-color" - OPT_HELP = "h:help" - OPT_VER = "v:version" + OPT_DRY_RUN = "D:dry-run" + OPT_LIST_PACKAGES = "L:list-packages" + OPT_DIR = "d:dir" + OPT_PATH = "p:path" + OPT_ERROR_DIR = "e:error-dir" + OPT_TAG = "t:tag" + OPT_QUIET = "q:quiet" + OPT_INGORE_PACKAGES = "ip:ignore-packages" + OPT_NO_CLEANUP = "nl:no-cleanup" + OPT_NO_COLOR = "nc:no-color" + OPT_HELP = "h:help" + OPT_VER = "v:version" OPT_COMPLETION = "completion" ) @@ -59,16 +61,18 @@ const ( // ////////////////////////////////////////////////////////////////////////////////// // var optMap = options.Map{ - OPT_DIR: {}, - OPT_ERROR_DIR: {}, - OPT_TAG: {Mergeble: true}, - OPT_QUIET: {Type: options.BOOL}, - OPT_DRY_RUN: {Type: options.BOOL}, - OPT_LIST_PACKAGES: {Type: options.BOOL}, - OPT_NO_CLEANUP: {Type: options.BOOL}, - OPT_NO_COLOR: {Type: options.BOOL}, - OPT_HELP: {Type: options.BOOL, Alias: "u:usage"}, - OPT_VER: {Type: options.BOOL, Alias: "ver"}, + OPT_DRY_RUN: {Type: options.BOOL}, + OPT_LIST_PACKAGES: {Type: options.BOOL}, + OPT_DIR: {}, + OPT_PATH: {}, + OPT_ERROR_DIR: {}, + OPT_TAG: {Mergeble: true}, + OPT_QUIET: {Type: options.BOOL}, + OPT_INGORE_PACKAGES: {Type: options.BOOL}, + OPT_NO_CLEANUP: {Type: options.BOOL}, + OPT_NO_COLOR: {Type: options.BOOL}, + OPT_HELP: {Type: options.BOOL, Alias: "u:usage"}, + OPT_VER: {Type: options.BOOL, Alias: "ver"}, OPT_COMPLETION: {}, } @@ -126,6 +130,15 @@ func configureUI() { // configureSubsystems configures bibop subsystems func configureSubsystems() { req.Global.SetUserAgent(APP, VER) + + if options.Has(OPT_PATH) { + newPath := os.Getenv("PATH") + ":" + options.GetS(OPT_PATH) + err := os.Setenv("PATH", newPath) + + if err != nil { + printErrorAndExit(err.Error()) + } + } } // validateOptions validates options @@ -199,14 +212,7 @@ func process(file string) { // validate validates recipe and print validation errors func validate(e *executor.Executor, r *recipe.Recipe, tags []string) { - vc := &executor.ValidationConfig{Tags: tags} - - if options.GetB(OPT_DRY_RUN) { - vc.IgnoreDependencies = true - vc.IgnorePrivileges = true - } - - errs := e.Validate(r, vc) + errs := e.Validate(r, getValidationConfig(tags)) if len(errs) == 0 { if options.GetB(OPT_DRY_RUN) { @@ -239,6 +245,22 @@ func listPackages(pkgs []string) { os.Exit(0) } +// getValidationConfig generates validation config +func getValidationConfig(tags []string) *executor.ValidationConfig { + vc := &executor.ValidationConfig{Tags: tags} + + if options.GetB(OPT_DRY_RUN) { + vc.IgnoreDependencies = true + vc.IgnorePrivileges = true + } + + if options.GetB(OPT_INGORE_PACKAGES) { + vc.IgnoreDependencies = true + } + + return vc +} + // printError prints error message to console func printError(f string, a ...interface{}) { fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) @@ -266,12 +288,14 @@ func showUsage() { func genUsage() *usage.Info { info := usage.NewInfo("", "recipe") + info.AddOption(OPT_DRY_RUN, "Parse and validate recipe") + info.AddOption(OPT_LIST_PACKAGES, "List required packages") info.AddOption(OPT_DIR, "Path to working directory", "dir") + info.AddOption(OPT_PATH, "Path to directory with binaries", "path") info.AddOption(OPT_ERROR_DIR, "Path to directory for errors data", "dir") info.AddOption(OPT_TAG, "Command tag", "tag") info.AddOption(OPT_QUIET, "Quiet mode") - info.AddOption(OPT_DRY_RUN, "Parse and validate recipe") - info.AddOption(OPT_LIST_PACKAGES, "List required packages") + info.AddOption(OPT_INGORE_PACKAGES, "Do not check system for installed packages") info.AddOption(OPT_NO_CLEANUP, "Disable deleting files created during tests") info.AddOption(OPT_NO_COLOR, "Disable colors in output") info.AddOption(OPT_HELP, "Show this help message") diff --git a/cli/executor/executor.go b/cli/executor/executor.go index b973239a..c3c9bda1 100644 --- a/cli/executor/executor.go +++ b/cli/executor/executor.go @@ -37,7 +37,7 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // -const MAX_STORAGE_SIZE = 2 * 1024 * 1024 // 2 MB +const MAX_STORAGE_SIZE = 8 * 1024 * 1024 // 2 MB // ////////////////////////////////////////////////////////////////////////////////// // @@ -201,10 +201,6 @@ func processRecipe(e *Executor, r *recipe.Recipe, tags []string) { printCommandHeader(e, command) ok := runCommand(e, command) - if index+1 != len(r.Commands) && !e.config.Quiet { - fmtc.NewLine() - } - e.skipped-- if !ok { @@ -217,6 +213,10 @@ func processRecipe(e *Executor, r *recipe.Recipe, tags []string) { e.passes++ } + if index+1 != len(r.Commands) && !e.config.Quiet { + fmtc.NewLine() + } + if r.Delay > 0 { time.Sleep(timeutil.SecondsToDuration(r.Delay)) } @@ -282,24 +282,21 @@ func runCommand(e *Executor, c *recipe.Command) bool { // execCommand executes command func execCommand(c *recipe.Command, outputStore *output.Store) (*exec.Cmd, io.Writer, error) { - var cmd *exec.Cmd + cmd, err := createCommand(c) - if c.User == "" { - cmdArgs := c.GetCmdlineArgs() - cmd = exec.Command(cmdArgs[0], cmdArgs[1:]...) - } else { - if !system.IsUserExist(c.User) { - return nil, nil, fmt.Errorf("Can't execute the command: user %s doesn't exist on the system", c.User) - } - - cmd = exec.Command("/sbin/runuser", "-s", "/bin/bash", c.User, "-c", c.GetCmdline()) + if err != nil { + return nil, nil, err } - input, _ := cmd.StdinPipe() + input, err := cmd.StdinPipe() + + if err != nil { + return nil, nil, err + } connectOutputStore(cmd, outputStore) - err := cmd.Start() + err = cmd.Start() if err != nil { return nil, nil, err @@ -310,6 +307,33 @@ func execCommand(c *recipe.Command, outputStore *output.Store) (*exec.Cmd, io.Wr return cmd, input, nil } +// createCommand creates command +func createCommand(c *recipe.Command) (*exec.Cmd, error) { + var cmdSlice []string + + if c.User != "" { + if !system.IsUserExist(c.User) { + return nil, fmt.Errorf("Can't execute the command: user %s doesn't exist on the system", c.User) + } + + cmdSlice = append(cmdSlice, "/sbin/runuser", "-s", "/bin/bash", c.User, "-c") + + if c.Recipe.Unbuffer { + cmdSlice = append(cmdSlice, "stdbuf -o0 -e0 -i0 "+c.GetCmdline()) + } else { + cmdSlice = append(cmdSlice, c.GetCmdline()) + } + } else { + if c.Recipe.Unbuffer { + cmdSlice = append(cmdSlice, "stdbuf", "-o0", "-e0", "-i0") + } + + cmdSlice = append(cmdSlice, c.GetCmdlineArgs()...) + } + + return exec.Command(cmdSlice[0], cmdSlice[1:]...), nil +} + // printBasicRecipeInfo print path to recipe and working dir func printBasicRecipeInfo(e *Executor, r *recipe.Recipe) { if e.config.Quiet { @@ -328,6 +352,7 @@ func printBasicRecipeInfo(e *Executor, r *recipe.Recipe) { printRecipeOptionFlag("Require root", r.RequireRoot) printRecipeOptionFlag("Fast finish", r.FastFinish) printRecipeOptionFlag("Lock workdir", r.LockWorkdir) + printRecipeOptionFlag("Unbuffered IO", r.Unbuffer) } // printResultInfo print info about finished test @@ -411,6 +436,10 @@ func runAction(a *recipe.Action, cmd *exec.Cmd, input io.Writer, outputStore *ou return action.Exit(a, cmd) case recipe.ACTION_EXPECT: return action.Expect(a, outputStore) + case recipe.ACTION_EXPECT_STDOUT: + return action.ExpectStdout(a, outputStore) + case recipe.ACTION_EXPECT_STDERR: + return action.ExpectStderr(a, outputStore) case recipe.ACTION_PRINT: return action.Input(a, input, outputStore) case recipe.ACTION_WAIT_OUTPUT: @@ -443,20 +472,25 @@ func connectOutputStore(cmd *exec.Cmd, outputStore *output.Store) { stdoutReader, _ := cmd.StdoutPipe() stderrReader, _ := cmd.StderrPipe() - go func(stdout, stderr io.Reader, outputStore *output.Store) { - for { - if outputStore.Clear { - outputStore.Purge() - } + go outputIOLoop(cmd, stdoutReader, outputStore.Stdout) + go outputIOLoop(cmd, stderrReader, outputStore.Stderr) +} - outputStore.Stdout.Write(ioutil.ReadAll(stdout)) - outputStore.Stderr.Write(ioutil.ReadAll(stderr)) +// outputIOLoop reads data from reader and writes it to output store container +func outputIOLoop(cmd *exec.Cmd, r io.Reader, c *output.Container) { + buf := make([]byte, 16384) - if cmd.ProcessState != nil && cmd.ProcessState.Exited() { - return - } + for { + n, _ := r.Read(buf[:cap(buf)]) + + if n > 0 { + c.Write(buf[:n]) } - }(stdoutReader, stderrReader, outputStore) + + if cmd.ProcessState != nil && cmd.ProcessState.Exited() { + return + } + } } // formatActionName format action name diff --git a/cli/executor/validators.go b/cli/executor/validators.go index 4de7f4b7..6aed5cc3 100644 --- a/cli/executor/validators.go +++ b/cli/executor/validators.go @@ -132,7 +132,7 @@ func checkPackages(r *recipe.Recipe) []error { } switch systemInfo.Distribution { - case system.LINUX_CENTOS: + case system.LINUX_CENTOS, system.LINUX_RHEL, system.LINUX_FEDORA: return checkRPMPackages(r.Packages) default: return nil diff --git a/output/output_store.go b/output/output_store.go index 6b86ad12..563e1285 100644 --- a/output/output_store.go +++ b/output/output_store.go @@ -18,7 +18,6 @@ import ( type Store struct { Stdout *Container Stderr *Container - Clear bool } type Container struct { @@ -37,14 +36,13 @@ func NewStore(size int) *Store { return &Store{ &Container{size: size}, &Container{size: size}, - false, } } // ////////////////////////////////////////////////////////////////////////////////// // // Write writes data into buffer -func (c *Container) Write(data []byte, _ error) { +func (c *Container) Write(data []byte) { if len(data) == 0 { return } diff --git a/parser/parser.go b/parser/parser.go index 56dcb415..bcf176f2 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -208,6 +208,9 @@ func applyGlobalOptions(r *recipe.Recipe, e *entity, line uint16) error { case recipe.OPTION_LOCK_WORKDIR: r.LockWorkdir, err = getOptionBoolValue(e.info.Keyword, e.args[0]) + case recipe.OPTION_UNBUFFER: + r.Unbuffer, err = getOptionBoolValue(e.info.Keyword, e.args[0]) + case recipe.OPTION_DELAY: r.Delay, err = getOptionFloatValue(e.info.Keyword, e.args[0]) } diff --git a/parser/parser_test.go b/parser/parser_test.go index 91e448b0..050b9cb2 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -81,6 +81,7 @@ func (s *ParseSuite) TestBasicParsing(c *C) { c.Assert(recipe.RequireRoot, Equals, true) c.Assert(recipe.FastFinish, Equals, true) c.Assert(recipe.LockWorkdir, Equals, false) + c.Assert(recipe.Unbuffer, Equals, true) c.Assert(recipe.Delay, Equals, 1.23) c.Assert(recipe.Commands, HasLen, 2) c.Assert(recipe.Packages, DeepEquals, []string{"package1", "package2"}) diff --git a/recipe/recipe.go b/recipe/recipe.go index 58978468..f504bbb7 100644 --- a/recipe/recipe.go +++ b/recipe/recipe.go @@ -31,6 +31,7 @@ type Recipe struct { RequireRoot bool // Require root privileges FastFinish bool // Fast finish flag LockWorkdir bool // Locking workdir flag + Unbuffer bool // Disabled IO buffering Delay float64 // Delay between commands Packages []string // Package list Commands []*Command // Commands diff --git a/recipe/tokens.go b/recipe/tokens.go index 6c634114..03632258 100644 --- a/recipe/tokens.go +++ b/recipe/tokens.go @@ -16,12 +16,15 @@ const ( OPTION_REQUIRE_ROOT = "require-root" OPTION_FAST_FINISH = "fast-finish" OPTION_LOCK_WORKDIR = "lock-workdir" + OPTION_UNBUFFER = "unbuffer" OPTION_DELAY = "delay" ACTION_EXIT = "exit" ACTION_WAIT = "wait" ACTION_EXPECT = "expect" + ACTION_EXPECT_STDOUT = "expect-stdout" + ACTION_EXPECT_STDERR = "expect-stderr" ACTION_WAIT_OUTPUT = "wait-output" ACTION_OUTPUT_MATCH = "output-match" ACTION_OUTPUT_CONTAINS = "output-contains" @@ -115,12 +118,15 @@ var Tokens = []TokenInfo{ {OPTION_REQUIRE_ROOT, 1, 1, true, false}, {OPTION_FAST_FINISH, 1, 1, true, false}, {OPTION_LOCK_WORKDIR, 1, 1, true, false}, + {OPTION_UNBUFFER, 1, 1, true, false}, {OPTION_DELAY, 1, 1, true, false}, {ACTION_EXIT, 1, 2, false, true}, {ACTION_WAIT, 1, 1, false, false}, {ACTION_EXPECT, 1, 2, false, false}, + {ACTION_EXPECT_STDOUT, 1, 2, false, false}, + {ACTION_EXPECT_STDERR, 1, 2, false, false}, {ACTION_WAIT_OUTPUT, 1, 1, false, false}, {ACTION_OUTPUT_MATCH, 1, 1, false, true}, {ACTION_OUTPUT_CONTAINS, 1, 1, false, true}, diff --git a/testdata/test1.recipe b/testdata/test1.recipe index 458ba9ef..b1bc7b5f 100644 --- a/testdata/test1.recipe +++ b/testdata/test1.recipe @@ -6,6 +6,7 @@ unsafe-actions yes require-root no fast-finish yes lock-workdir no +unbuffer yes delay 1.23 var user nobody