diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..e01a4e4 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,96 @@ +version: 2 +jobs: + build: + docker: + - image: ubuntu:latest + working_directory: /root/singnet/src/github.com/singnet/platform-pipeline + environment: + GOPATH: /root/singnet + SINGNET_REPOS: /root/singnet/src/github.com/singnet + steps: + - run: + name: Install tools + command: | + export PATH=$PATH:$GOPATH/bin + mkdir $GOPATH/download + cd $GOPATH/download + apt-get update + apt-get -y install sudo wget git + # Install NodeJS toolset + sudo apt-get -y install nodejs npm + # install Go tools + sudo apt-get -y install golang go-dep golang-goprotobuf-dev golint + # install IPFS + wget https://dist.ipfs.io/go-ipfs/v0.4.17/go-ipfs_v0.4.17_linux-amd64.tar.gz + tar xvfz go-ipfs_*.tar.gz + cp ./go-ipfs/ipfs /usr/local/bin + # Installl Python + sudo apt-get -y install python3 python3-pip + # Install other + sudo apt-get -y install libudev-dev libusb-1.0-0-dev + - run: + name: Build token-contracts + command: | + cd $SINGNET_REPOS + git clone https://github.com/singnet/token-contracts.git + cd token-contracts + npm install + npm run-script compile + npm run-script package-npm + - run: + name: Build platform-contracts + command: | + cd $SINGNET_REPOS + git clone https://github.com/singnet/platform-contracts.git + cd platform-contracts + npm install ganache-cli + # install token-contracts from local dir + npm install $SINGNET_REPOS/token-contracts/build/npm-module + npm install + npm run-script compile + npm run-script package-npm + - run: + name: Build snet-cli + command: | + cd $SINGNET_REPOS + git clone https://github.com/singnet/snet-cli.git + cd snet-cli + # install token-contracts and platform-contracts from local dir + cd blockchain/ + npm install $SINGNET_REPOS/token-contracts/build/npm-module + npm install $SINGNET_REPOS/platform-contracts/build/npm-module + cd .. + ./scripts/blockchain install + pip3 install -e . + - run: + name: Build snet-daemon + command: | + export PATH=$PATH:$GOPATH/bin + cd $SINGNET_REPOS + git clone https://github.com/singnet/snet-daemon.git + cd snet-daemon + # install token-contracts and platform-contracts from local dir + cd resources/blockchain + npm install $SINGNET_REPOS/token-contracts/build/npm-module + npm install $SINGNET_REPOS/platform-contracts/build/npm-module + cd ../.. + ./scripts/install + ./scripts/build linux amd64 + - run: + name: Build example-service + command: | + export PATH=$PATH:$GOPATH/bin + cd $SINGNET_REPOS + git clone https://github.com/singnet/example-service.git + cd example-service + pip3 install -r requirements.txt + - checkout + - run: + name: Run integration tests + command: | + export PATH=$PATH:$GOPATH/bin + mkdir $GOPATH/log + go get github.com/DATA-DOG/godog/cmd/godog + # Disable TensorFlow warnings wich pollute example-service log file + export TF_CPP_MIN_LOG_LEVEL=2 + godog diff --git a/features/publish_example_service.feature b/features/publish_example_service.feature new file mode 100644 index 0000000..22b10d1 --- /dev/null +++ b/features/publish_example_service.feature @@ -0,0 +1,24 @@ +Feature: Publish example service + + Scenario: Run all services and publish example service + Given Ethereum network is running on port 8545 + Given Contracts are deployed using Truffle + Given IPFS is running with API port 5002 and Gateway port 8081 + Given Identity is created with user "snet-user" and private key "0xc71478a6d0fe44e763649de0a0deb5a080b788eefbbcf9c6f7aef0dd5dbd67e0" + Given snet is configured with Ethereum RPC endpoint 8545 + Given snet is configured with IPFS endpoint 5002 + When Organization is added: + | organization | address | member | + | ExampleOrganization | 0x4e74fefa82e83e0964f0d9f53c68e03f7298a8b2 | 0x3b2b3c2e2e7c93db335e69d827f3cc4bc2a2a2cb | + When example-service is registered + | name | price | endpoint | tags | description | + | ExampleOrganization | 1 | http://localhost:8080 | example service | Example service | + When example-service is published to network + | agent factory address | registry address | + | 0x5c7a4290f6f8ff64c69eeffdfafc8644a4ec3a4e | 0x4e74fefa82e83e0964f0d9f53c68e03f7298a8b2 | + When example-service is run with snet-daemon + | daemon port | ethereum endpoint port | passthrough endpoint port | agent contract address | private key | + | 8080 | 8545 | 5001 | 0xD39321C654351b412F4D13B45E7020FE9f99f608 | ba398df3130586b0d5e6ef3f757bf7fe8a1299d4b7268fdaae415952ed30ba87 | + Then SingularityNET job is created + | max price | agent contract address | + | 100000000 | 0xD39321C654351b412F4D13B45E7020FE9f99f608 | diff --git a/publish_example_service_test.go b/publish_example_service_test.go new file mode 100644 index 0000000..9d628d7 --- /dev/null +++ b/publish_example_service_test.go @@ -0,0 +1,475 @@ +package main + +import ( + "errors" + "fmt" + "log" + "strings" + "time" + + "github.com/DATA-DOG/godog" + "github.com/DATA-DOG/godog/gherkin" +) + +var outputFile string +var outputContainsStrings []string + +var platformContractsDir string +var exampleServiceDir string +var snetConfigFile string + +func init() { + platformContractsDir = envSingnetRepos + "/platform-contracts" + exampleServiceDir = envSingnetRepos + "/example-service" + snetConfigFile = envHome + "/.snet/config" +} + +func ethereumNetworkIsRunningOnPort(port int) error { + + outputFile = logPath + "/ganache.log" + outputContainsStrings = []string{"Listening on 127.0.0.1:" + toString(port)} + + args := []string{"--mnemonic", "gauge enact biology destroy normal tunnel slight slide wide sauce ladder produce"} + command := ExecCommand{ + Command: "./node_modules/.bin/ganache-cli", + Directory: platformContractsDir, + OutputFile: outputFile, + Args: args, + } + + err := runCommandAsync(command) + + if err != nil { + return err + } + + exists, err := checkWithTimeout(5000, 500, checkFileContainsStrings) + if err != nil { + return err + } + + if !exists { + return errors.New("Etherium networks is not started") + } + + return nil +} + +func contractsAreDeployedUsingTruffle() error { + + command := ExecCommand{ + Command: "./node_modules/.bin/truffle", + Directory: platformContractsDir, + Args: []string{"compile"}, + } + + err := runCommand(command) + + if err != nil { + return err + } + + command.Args = []string{"migrate", "--network", "local"} + err = runCommand(command) + + return err +} + +func ipfsIsRunning(portAPI int, portGateway int) error { + + env := []string{"IPFS_PATH=" + envGoPath + "/ipfs"} + + command := ExecCommand{ + Command: "ipfs", + Env: env, + Args: []string{"init"}, + } + + err := runCommand(command) + + if err != nil { + return err + } + + command.Args = []string{"bootstrap", "rm", "--all"} + err = runCommand(command) + + if err != nil { + return err + } + + addressAPI := "/ip4/127.0.0.1/tcp/" + toString(portAPI) + command.Args = []string{"config", "Addresses.API", addressAPI} + err = runCommand(command) + + if err != nil { + return err + } + + addressGateway := "/ip4/0.0.0.0/tcp/" + toString(portGateway) + command.Args = []string{"config", "Addresses.Gateway", addressGateway} + err = runCommand(command) + + if err != nil { + return err + } + + outputFile = logPath + "/ipfs.log" + command.OutputFile = outputFile + command.Args = []string{"daemon"} + err = runCommandAsync(command) + + if err != nil { + return err + } + + outputContainsStrings = []string{ + "Daemon is ready", + "server listening on " + addressAPI, + "server listening on " + addressGateway, + } + exists, err := checkWithTimeout(5000, 500, checkFileContainsStrings) + + if err != nil { + return err + } + + if !exists { + return errors.New("Etherium networks is not started") + } + + return nil +} + +func identityIsCreatedWithUserAndPrivateKey(user string, privateKey string) error { + + command := ExecCommand{ + Command: "snet", + Args: []string{"identity", "create", user, "key", "--private-key", privateKey}, + } + err := runCommand(command) + + if err != nil { + return err + } + + command.Args = []string{"identity", "snet-user"} + return runCommand(command) +} + +func snetIsConfiguredWithEthereumRPCEndpoint(endpointEthereumRPC int) error { + + config := ` +[network.local] +default_eth_rpc_endpoint = http://localhost:` + toString(endpointEthereumRPC) + + err := appendToFile(snetConfigFile, config) + + if err != nil { + return err + } + + command := ExecCommand{ + Command: "snet", + Args: []string{"network", "local"}, + } + err = runCommand(command) + + if err != nil { + return err + } + + outputFile = snetConfigFile + outputContainsStrings = []string{"session"} + exists, e := checkWithTimeout(5000, 500, checkFileContainsStrings) + + if !exists { + return errors.New("snet config file is not created: " + snetConfigFile) + } + + return e +} + +func snetIsConfiguredWithIPFSEndpoint(endpointIPFS int) error { + + command := ExecCommand{ + Command: "sed", + Args: []string{"-ie", "/ipfs/,+2d", snetConfigFile}, + } + + err := runCommand(command) + + if err != nil { + return err + } + + config := ` +[ipfs] +default_ipfs_endpoint = http://localhost:` + toString(endpointIPFS) + + return appendToFile(snetConfigFile, config) +} + +func organizationIsAdded(table *gherkin.DataTable) error { + + organization := getTableValue(table, "organization") + address := getTableValue(table, "address") + member := getTableValue(table, "member") + + args := []string{ + "contract", "Registry", + "--at", address, + "createOrganization", organization, + "[\"" + member + "\"]", + "--transact", + } + + command := ExecCommand{ + Command: "snet", + Input: []string{"y"}, + Args: args, + } + + return runCommand(command) +} + +func exampleserviceIsRegistered(table *gherkin.DataTable) error { + + name := getTableValue(table, "name") + price := getTableValue(table, "price") + endpoint := getTableValue(table, "endpoint") + tags := getTableValue(table, "tags") + description := getTableValue(table, "description") + + command := ExecCommand{ + Command: "snet", + Directory: exampleServiceDir, + Input: []string{"", "", name, "", price, endpoint, tags, description}, + Args: []string{"service", "init"}, + } + + return runCommand(command) +} + +func exampleserviceIsPublishedToNetwork(table *gherkin.DataTable) error { + + agentFactoryAddress := getTableValue(table, "agent factory address") + registryAddress := getTableValue(table, "registry address") + + args := []string{ + "service", "publish", "local", + "--config", "./service.json", + "--agent-factory-at", agentFactoryAddress, + "--registry-at", registryAddress, + } + + command := ExecCommand{ + Command: "snet", + Directory: exampleServiceDir, + Input: []string{"y", "y"}, + Args: args, + } + + return runCommand(command) +} + +func exampleserviceIsRunWithSnetdaemon(table *gherkin.DataTable) error { + + daemonPort := getTableValue(table, "daemon port") + ethereumEndpointPort := getTableValue(table, "ethereum endpoint port") + passthroughEndpointPort := getTableValue(table, "passthrough endpoint port") + + agentContractAddress := getTableValue(table, "agent contract address") + privateKey := getTableValue(table, "private key") + + snetdConfigTemplate := ` + { + "AGENT_CONTRACT_ADDRESS": "%s", + "AUTO_SSL_DOMAIN": "", + "AUTO_SSL_CACHE_DIR": "", + "BLOCKCHAIN_ENABLED": true, + "CONFIG_PATH": "", + "DAEMON_LISTENING_PORT": %s, + "DAEMON_TYPE": "grpc", + "DB_PATH": "./db", + "ETHEREUM_JSON_RPC_ENDPOINT": "http://localhost:%s", + "EXECUTABLE_PATH": "", + "LOG_LEVEL": 5, + "PASSTHROUGH_ENABLED": true, + "PASSTHROUGH_ENDPOINT": "http://localhost:%s", + "POLL_SLEEP": "", + "PRIVATE_KEY": "%s", + "SERVICE_TYPE": "jsonrpc", + "SSL_CERT": "", + "SSL_KEY": "", + "WIRE_ENCODING": "json" + }` + + snetdConfig := fmt.Sprintf(snetdConfigTemplate, + agentContractAddress, daemonPort, ethereumEndpointPort, passthroughEndpointPort, privateKey) + + file := exampleServiceDir + "/snetd.config.json" + err := writeToFile(file, snetdConfig) + + if err != nil { + return err + } + + linkFile(envSingnetRepos+"/snet-daemon/build/snetd-linux-amd64", exampleServiceDir+"/snetd-linux-amd64") + + outputFile = logPath + "/example-service.log" + outputContainsStrings = []string{} + + command := ExecCommand{ + Command: exampleServiceDir + "/scripts/run-snet-service", + Directory: exampleServiceDir, + OutputFile: outputFile, + } + + err = runCommandAsync(command) + + if err != nil { + return err + } + + _, err = checkWithTimeout(5000, 500, checkFileContainsStrings) + + if err != nil { + return err + } + + time.Sleep(2 * time.Second) + + command = ExecCommand{ + Command: exampleServiceDir + "/scripts/test-call", + Directory: exampleServiceDir, + } + + return runCommand(command) +} + +func singularityNETJobIsCreated(table *gherkin.DataTable) error { + + agentContractAddress := getTableValue(table, "agent contract address") + maxPrice := getTableValue(table, "max price") + + args := []string{ + "agent", + "--at", agentContractAddress, + "create-jobs", + "--funded", + "--signed", + "--max-price", maxPrice, + } + + command := ExecCommand{ + Command: "snet", + Directory: exampleServiceDir, + Input: []string{"y", "y", "y"}, + Args: args, + } + + err := runCommand(command) + + if err != nil { + return err + } + + args = []string{ + "client", "call", "classify", + fmt.Sprintf(`{"image_type": "jpg", "image": "%s"}`, testImage), + "--agent-at", agentContractAddress, + } + + command = ExecCommand{ + Command: "snet", + Directory: exampleServiceDir, + Args: args, + } + + return runCommand(command) +} + +func FeatureContext(s *godog.Suite) { + s.Step(`^Ethereum network is running on port (\d+)$`, ethereumNetworkIsRunningOnPort) + s.Step(`^Contracts are deployed using Truffle$`, contractsAreDeployedUsingTruffle) + s.Step(`^IPFS is running with API port (\d+) and Gateway port (\d+)$`, ipfsIsRunning) + s.Step(`^Identity is created with user "([^"]*)" and private key "([^"]*)"$`, + identityIsCreatedWithUserAndPrivateKey) + s.Step(`^snet is configured with Ethereum RPC endpoint (\d+)$`, snetIsConfiguredWithEthereumRPCEndpoint) + s.Step(`^snet is configured with IPFS endpoint (\d+)$`, snetIsConfiguredWithIPFSEndpoint) + s.Step(`^Organization is added:$`, organizationIsAdded) + s.Step(`^example-service is registered$`, exampleserviceIsRegistered) + s.Step(`^example-service is published to network$`, exampleserviceIsPublishedToNetwork) + s.Step(`^example-service is run with snet-daemon$`, exampleserviceIsRunWithSnetdaemon) + s.Step(`^SingularityNET job is created$`, singularityNETJobIsCreated) + +} + +func checkFileContainsStrings() (bool, error) { + + log.Printf("check output file: '%s'\n", outputFile) + log.Printf("check output file contains string: '%s'\n", strings.Join(outputContainsStrings, ",")) + + out, err := readFile(outputFile) + if err != nil { + return false, err + } + + if out != "" { + log.Printf("Output: %s\n", out) + } + + if strings.Contains(out, "Error") { + return false, errors.New("Output contains error") + } + + for _, str := range outputContainsStrings { + if !strings.Contains(out, str) { + return false, nil + } + } + + return true, nil +} + +func getTableValue(table *gherkin.DataTable, column string) string { + + names := table.Rows[0].Cells + for i, cell := range names { + if cell.Value == column { + return table.Rows[1].Cells[i].Value + } + } + + log.Printf("column: %s has not been found in table", column) + return "" +} + +var testImage = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxAPDw0PDQ0PDg0PDQ0PDQ0PDQ8ODQ0NFRIWFhUSExUYHyghGB4lJxMWITEhJSor" + + "Li4uGB8zODMsNygtLisBCgoKDQ0NDxAPFSsdFRktLSs4LCsrKysrKysrKzcrLSsrKy0tKzcrKysrKysrKystNy0rKysrKysrKysr" + + "KysrK//AABEIAOEA4QMBIgACEQEDEQH/xAAcAAEAAQUBAQAAAAAAAAAAAAAAAQIDBAUGBwj/xABAEAEAAgECAgcEBAoLAQAAAAAA" + + "AQIDBBEFIQYHEhMxQWFRcZGxMkKBghQiI3KDkqGissEIF1NUYpOjwtHh8BX/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAQL/xAAWEQEB" + + "AQAAAAAAAAAAAAAAAAAAEQH/2gAMAwEAAhEDEQA/APcQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AEJavjHGKabsRe1K3vv2O3aKVnbbfn9scgbRG7jtV0g1XjSKxWfCa0i1fjzYF+OaqfHPMfmxWPkD0Dc3ee//AEs0/SzXn70q6ay0" + + "+N7frSlHf7m7iseaZ+tPxlk4r+s/GVHWG7n8NLz4Tf8AWmGTSuSNvyto+9MiVuBqq58sfW329tYU243XHMRmmkbzFYmLecztHKRW" + + "3EQkAAAAAAAAAAAAAABby56UibXvWlY8bWmKxH2y13SPjWPRae+fJtO3KlZtFe3f2b+UcpmZ8oiXzv0g41rOOavutPGfUT2prWkX" + + "tGGN5+rjjlWvrbefOdvAH0Nk6T6Cv0tdp4/TUl551q8c0mpppq6fU481/wAtE1pbteVZ5uMp1McVmsTP4LW08+xOptvHpvFWB0g6" + + "DcQ4Vp41GpjT1xfhGPfusk5LTNpiu2+0bRyhUYmk4jk0/b7jJOObREb18vWPZ4s7F0y1lYiLXrl287d5Fp98xZps/jLGmVSOjv03" + + "1EzMzHYjs8opett7b+M9uszt6Ndbp7xKsztfBNd+W+Cm+3q02SWLkNV0tesjicf3f/Jj/lcjrO4p7dNH6H/tyMoQdl/Wjxfy1GGv" + + "l+LhhTbrH4raPxtZbf8Aw1rVx6uAdFm6Wa/J9PWZ538fykx8mz6LZb5dXpu8vbJPfYtptabfXj2uQxO06vcXb12jr7dRh393bjcH" + + "0qECKAAAAAAAAAAAAAA8O/pBcbvXLp9JWZikaeMt9vC1r3tG3+nHxbrqI4TSmnz55rE5N6Ui3nG9Ztaft3j4Of8A6RXDLxl0urrE" + + "93fDGG0+UXpe1o+MX/Y3HUXxuk0y6e1oi2SKZMUT52rE1vX38o+Erg9ecV1xaLvuC62IjnSMeSPSa3iXaxLXdI9H3+j1WKY37eDJ" + + "G3rtvHyQfLVrdqtbR9albfGIY9pXcNdsVYnxpNqT762mv8li0tIoySxrr1pWLoKJQlAEKoUK4BfxPSeqLS95r8M+VO1f3bVnb+Tz" + + "XD4va+ozQ721OaY5Ux0pE+tpmZ/hQevwAKAAAAAAAAAAAAAA03Szo7i4lpMulz8otzx5Ij8bFlj6N4/94TL5u4jwniHANVtel+xF" + + "+1iy07Vcd4ieVsd48J9PH5vqpj63RYs9Jx58VMuO0bWpkrF6z9kg8e4P14460rXV6bJa8RtN68pn37bxLbf138PmOeDUe6YiGx4t" + + "1P8AC802tipl0lp/sctrUj3UvvEfY4/inUXkjedJrcWSee1NRinHM/epv8io4HUWre2ovjiYx31WovjiY2mMd8k2pvHumGtyOy4l" + + "0O1ugwXnXY6xvetaXpkjJS0RTaNp8fKPGHH6iNpUY9lmy5ZasCiUJlAJhMKd1UAyNNG9ofS/VTwzuOG4rTG1s8zln29nwr8v2vn7" + + "olwy2q1enwUjnkyRX3R5z9kRMvqzSYK4sePHSNqY6UpWPZWsbQir4jc3BIjdIAAAAAAAAAAAAAAAANV0m4VGs0ubBPjakzjn2ZI5" + + "1l8x8b0VsOTJS9drVtatonymJ2fWEw8f65OjG141uGm9cnLP2a8q5IjlafZv84XDXit1qWTnpsxpEUShMqQSrqt7tl0f4Xk1eoxY" + + "MUb3yWiPbFY352n0jxQeudRnAezGXX5K7bTOHTzPtmI7do+O3xevd40PCNPTS4MOnxRtjxUiseU2nztPrM82dXNuK2PeJ7xg1uuR" + + "YGZFlcSxK2X6SC8IhIAAAAAAAAAAAAAACm9ItExaImJ8YmN4mFQDlOK9XfCtVM2y6GlbT9fDfJgn9yYhodR1M8Jn6P4VT0jUzaP3" + + "ol6SpsDyfN1K6D6up1cfexz/ALWPHUvoonnqdVMe/HH8nrlqLU4geZYOqDhlfpVz5PztRav8OzpODdEtJo940unri35WtG83mPW0" + + "zM/tdT3SYxA11NIvV07N7tVFAY1MK53a92U9kFqtF2tVUQkBKEgAAAAAAAAAAAAAAAAI2SApmDZUAp2TskBGxskBAkBAkAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//2Q==" diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..e9fc8fe --- /dev/null +++ b/utils.go @@ -0,0 +1,175 @@ +package main + +import ( + "errors" + "io/ioutil" + "log" + "os" + "os/exec" + "strconv" + "strings" + "time" +) + +type checkWithTimeoutType func() (bool, error) + +// ExecCommand is used to run command from command line +type ExecCommand struct { + Command string + Directory string + Env []string + Input []string + OutputFile string + Args []string +} + +var envHome string +var envSingnetRepos string +var envGoPath string +var logPath string + +func init() { + + envHome = os.Getenv("HOME") + envSingnetRepos = os.Getenv("SINGNET_REPOS") + envGoPath = os.Getenv("GOPATH") + logPath = envGoPath + "/log" + log.Printf("SINGNET_REPOS=%s\n", envSingnetRepos) +} + +func readFile(file string) (string, error) { + + buf, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + return string(buf), nil +} + +func fileExists(fileName string) bool { + + _, err := os.Stat(fileName) + return !os.IsNotExist(err) +} + +func writeToFile(fileName string, content string) error { + + file, err := os.Create(fileName) + if err != nil { + return err + } + + defer file.Close() + + _, err = file.WriteString(content) + return err +} + +func appendToFile(fileName string, content string) error { + + file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) + if err != nil { + return err + } + + defer file.Close() + + _, err = file.WriteString(content) + return err +} + +func linkFile(fileFrom string, fileTo string) error { + + if fileExists(fileTo) { + return nil + } + + return os.Symlink(fileFrom, fileTo) +} + +func runCommand(execCommad ExecCommand) error { + + log.Printf("[run_command] dir: '%s', command: '%s', args: '%s'\n", + execCommad.Directory, execCommad.Command, strings.Join(execCommad.Args, ",")) + + cmd, err := getCmd(execCommad) + + if err != nil { + return err + } + + return cmd.Run() +} + +func runCommandAsync(execCommad ExecCommand) error { + + log.Printf("[run_command_async] dir: '%s', command: '%s', args: '%s'\n", + execCommad.Directory, execCommad.Command, strings.Join(execCommad.Args, ",")) + + cmd, err := getCmd(execCommad) + + if err != nil { + return err + } + + return cmd.Start() +} + +func getCmd(execCommad ExecCommand) (*exec.Cmd, error) { + + cmd := exec.Command(execCommad.Command, execCommad.Args...) + cmd.Dir = execCommad.Directory + + if execCommad.OutputFile != "" { + stdOut, err := os.Create(execCommad.OutputFile) + if err != nil { + return nil, err + } + cmd.Stdout = stdOut + cmd.Stderr = stdOut + } else { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + if len(execCommad.Input) > 0 { + cmd.Stdin = strings.NewReader(strings.Join(execCommad.Input, "\n")) + } + + cmd.Env = os.Environ() + env := execCommad.Env + if env != nil && len(env) > 0 { + for _, e := range env { + cmd.Env = append(cmd.Env, e) + } + } + + return cmd, nil +} + +// timeout and tick are measurd in milliseconds units +func checkWithTimeout(timeout time.Duration, + tick time.Duration, + f checkWithTimeoutType) (bool, error) { + + _timeout := time.After(timeout * time.Millisecond) + _tick := time.Tick(tick * time.Millisecond) + + for { + select { + case <-_timeout: + return false, errors.New("timed out") + case <-_tick: + ok, err := f() + if err != nil { + return false, err + } else if ok { + return true, nil + } + } + } +} + +func toString(value int) string { + return strconv.Itoa(value) +}