module osal // import freeflowuniverse.herolib.core.texttools // import freeflowuniverse.herolib.core.pathlib import freeflowuniverse.herolib.ui.console import json import os import time // import io.util pub struct JobError { Error pub mut: job Job error_type ErrorType } pub enum ErrorType { exec timeout args } fn (err JobError) msg() string { if err.error_type == .args { return 'Error in arguments:\n${err.job.cmd}' } mut msg := '' if err.job.cmd.ignore_error { return 'Ignore error for ${err.job.cmd.scriptpath}\n' } else { if err.error_type == .timeout { return 'Execution failed timeout\n${err.job}' } msg = 'Execution failed with code ${err.job.exit_code}\n' if err.job.cmd.scriptpath.len > 0 { msg += '\nscript path:${err.job.cmd.scriptpath}' } if err.job.output.len > 0 { msg += '\n\n## stdout:\n${err.job.output}' } if err.job.error.len > 0 { msg += '\n\n## stderr:\n${err.job.error}' } } return msg } fn (err JobError) code() int { if err.error_type == .timeout { return 9999 } return err.job.exit_code } @[params] pub struct Command { pub mut: name string // to give a name to your command, good to see logs... cmd string description string timeout int = 3600 // timeout in sec stdout bool = true stdout_log bool = true raise_error bool = true // if false, will not raise an error but still error report ignore_error bool // means if error will just exit and not raise, there will be no error reporting work_folder string // location where cmd will be executed environment map[string]string // env variables ignore_error_codes []int scriptpath string // is the path where the script will be put which is executed scriptkeep bool // means we don't remove the script debug bool // if debug will put +ex in the script which is being executed and will make sure script stays shell bool // means we will execute it in a shell interactive retry int interactive bool = true async bool runtime RunTime } pub enum JobStatus { init running error_exec error_timeout error_args done } pub enum RunTime { bash python heroscript herocmd v } pub struct Job { pub mut: start time.Time end time.Time cmd Command output string error string exit_code int status JobStatus process ?&os.Process @[skip; str: skip] runnr int // nr of time it runs, is for retry } // cmd is the cmd to execute can use ' ' and spaces . // if \n in cmd it will write it to ext and then execute with bash . // if die==false then will just return returncode,out but not return error . // if stdout will show stderr and stdout . // . // if cmd starts with find or ls, will give to bash -c so it can execute . // if cmd has no path, path will be found . // . // Command argument: . //``` // name string // to give a name to your command, good to see logs... // cmd string // description string // timeout int = 3600 // timeout in sec // stdout bool = true // stdout_log bool = true // raise_error bool = true // if false, will not raise an error but still error report // ignore_error bool // means if error will just exit and not raise, there will be no error reporting // work_folder string // location where cmd will be executed // environment map[string]string // env variables // ignore_error_codes []int // scriptpath string // is the path where the script will be put which is executed // scriptkeep bool // means we don't remove the script // debug bool // if debug will put +ex in the script which is being executed and will make sure script stays // shell bool // means we will execute it in a shell interactive // retry int // interactive bool = true // make sure we run on non interactive way // async bool // runtime RunTime (.bash, .python) // // returns Job: // start time.Time // end time.Time // cmd Command // output []string // error []string // exit_code int // status JobStatus // process os.Process //``` // return Job . pub fn exec(cmd_ Command) !Job { mut cmd := cmd_ mut job := Job{ cmd: cmd } job.start = time.now() if job.cmd.debug { job.cmd.stdout = true console.print_header(' execute: ${job.cmd.cmd}') } if cmd.shell { // $if debug { // console.print_debug('cmd shell: ${cmd.cmd}') // } scriptpath := cmd_to_script_path(job.cmd)! os.execvp(scriptpath, [])! return job } if cmd.ignore_error { cmd.retry = 0 } if !cmd.async { job.execute_retry() or { // println(err) return err } } return job } // execute the job and wait on result // will retry as specified pub fn (mut job Job) execute_retry() ! { for x in 0 .. job.cmd.retry + 1 { job.execute() or { if x == job.cmd.retry { // println(job) return err } } // println(job) if job.status == .done { // means we could execute we can stop return } } job.close()! } // execute the job, start process, process will not be closed . // important you need to close the process later by job.close()! otherwise we get zombie processes pub fn (mut job Job) execute() ! { job.runnr += 1 job.start = time.now() job.status = .running job.cmd.scriptpath = cmd_to_script_path(job.cmd)! if job.cmd.debug { console.print_debug(job) } if job.cmd.debug { console.print_debug(' - process execute ${job.cmd.scriptpath}') } mut p := os.new_process(job.cmd.scriptpath) if job.cmd.work_folder.len > 0 { p.set_work_folder(job.cmd.work_folder) } if job.cmd.environment.len > 0 { if job.cmd.debug { console.print_debug(' - process setargs ${job.cmd.environment}') } p.set_environment(job.cmd.environment) } p.set_redirect_stdio() // p.set_args(process_args[1..process_args.len]) if job.cmd.stdout { console.print_debug('') } p.run() job.process = p // initial check, no point reading the output if we can't get the process starting // NOT OK TO DO BECAUSE IF PROCESS FINISHED WITHOUT ISSUE THEN NOT OK // if ! p.is_alive() { // if job.cmd.debug{ // console.print_debug(p) // console.print_stderr(' ########## Process could not get started, result code is: ${p.code}') // } // job.exit_code = p.code // job.status = .error_exec // job.cmd.scriptkeep = true // job.close()! // return // } job.wait()! } // ORDER IS // EXECUTE // LOOP -> WAIT -> PROCESS -> READ // -> CLOSE // wait till the job finishes or goes in error pub fn (mut job Job) wait() ! { if job.status != .running && job.status != .init { return error('can only wait for running job') } for { job.process()! // console.print_debug(result) if job.status == .done { // console.print_stderr("wait done") job.close()! return } time.sleep(10 * time.millisecond) } job.close()! } // process (read std.err and std.out of process) pub fn (mut job Job) process() ! { // $if debug{console.print_debug(" - job process: $job")} if job.status == .init { panic('should not be here') // job.execute()! } mut p := job.process or { return error('there is not process on job') } // mut result := job.read() if p.is_alive() { job.read()! // result=job.read()! if time.now().unix() > job.start.unix() + job.cmd.timeout * 1000 { // console.print_stderr("TIMEOUT TIMEOUT TIMEOUT TIMEOUT") if job.cmd.debug { console.print_stderr('***TIMEOUT TIMEOUT TIMEOUT TIMEOUT***') } p.signal_pgkill() p.close() job.exit_code = 9999 job.end = time.now() job.status = .error_timeout if job.cmd.raise_error { return JobError{ job: job error_type: .timeout } } } } else { if !job.cmd.ignore_error && job.cmd.debug { console.print_stderr(" - process stopped (don't know if error)") } job.read()! job.read()! job.status = .done if p.code > 0 { if job.cmd.debug { console.print_stderr(' ########## Process result code is > 0: ${p.code}') } job.exit_code = p.code job.status = .error_exec job.cmd.scriptkeep = true job.close()! } } } fn (mut job Job) read() ! { mut p := job.process or { return error('there is no process on job') } // console.print_debug("READ STDOUT") out_std := p.pipe_read(.stdout) or { '' } // console.print_debug(" OK") if out_std.len > 0 { if job.cmd.stdout { console.print_stdout(out_std) } job.output += out_std } // console.print_debug("READ ERROR") out_error := p.pipe_read(.stderr) or { '' } // console.print_debug(" OK") if out_error.len > 0 { if job.cmd.stdout && job.cmd.ignore_error == false { console.print_stderr(out_error) } job.error += out_error } } // will wait & close pub fn (mut job Job) close() ! { mut p := job.process or { return error('there is no process on job') } // console.print_debug("CLOSE") p.signal_pgkill() p.wait() p.close() job.end = time.now() if job.exit_code > 0 && job.exit_code !in job.cmd.ignore_error_codes { if !job.cmd.ignore_error { errorpath := job.cmd.scriptpath.all_before_last('.sh') + '_error.json' errorjson := json.encode_pretty(job) os.write_file(errorpath, errorjson) or { msg := 'cannot write errorjson to ${errorpath}' return error(msg) } errorpath2 := job.cmd.scriptpath.all_before_last('.sh') + '_error.log' mut errortxt := '# ERROR:\n\n' errortxt += job.cmd.cmd + '\n' errortxt += '## OUTPUT:\n\n' errortxt += job.output os.write_file(errorpath2, errortxt) or { msg := 'cannot write error to ${errorpath2}' return error(msg) } je := JobError{ job: job error_type: .exec } if job.cmd.stdout { console.print_debug('Job Error') console.print_debug(je.msg()) } if job.cmd.raise_error { return je } } } if job.exit_code == 0 && job.cmd.scriptkeep == false && os.exists(job.cmd.scriptpath) { // console.print_debug(job.cmd.scriptpath) os.rm(job.cmd.scriptpath)! } if (job.cmd.ignore_error || job.cmd.scriptkeep == false) && os.exists(job.cmd.scriptpath) { os.rm(job.cmd.scriptpath)! } if job.cmd.ignore_error { job.status = .done return } if job.cmd.raise_error && job.exit_code > 0 { return JobError{ job: job error_type: .exec } } } // shortcut to execute a job silent pub fn execute_silent(cmd string) !string { job := exec(cmd: cmd, stdout: false)! return job.output } pub fn execute_debug(cmd string) !string { job := exec(cmd: cmd, stdout: true, debug: true)! return job.output } // shortcut to execute a job to stdout pub fn execute_stdout(cmd string) !string { job := exec(cmd: cmd, stdout: true)! return job.output } // shortcut to execute a job interactive means in shell pub fn execute_interactive(cmd string) ! { exec(cmd: cmd, stdout: true, shell: true)! } // executes a cmd, if not error return true pub fn execute_ok(cmd string) bool { res := os.execute(cmd) if res.exit_code > 0 { return false } return true } pub fn cmd_exists(cmd string) bool { cmd1 := 'which ${cmd}' res := os.execute(cmd1) if res.exit_code > 0 { return false } return true } pub fn cmd_exists_profile(cmd string) bool { cmd1 := '${profile_path_source_and() or { panic(err) }} which ${cmd}' res := os.execute(cmd1) if res.exit_code > 0 { return false } return true } // cmd is the cmd to execute can use ' ' and spaces // if \n in cmd it will write it to ext and then execute with bash // if die==false then will just return returncode,out but not return error // if stdout will show stderr and stdout // // if cmd starts with find or ls, will give to bash -c so it can execute // if cmd has no path, path will be found // $... are remplaced by environment arguments TODO:implement // // Command argument: // cmd string // timeout int = 600 // stdout bool = true // die bool = true // debug bool // // return what needs to be executed can give it to bash -c ... pub fn exec_string(cmd Command) !string { mut job := Job{ cmd: cmd } job.start = time.now() job.cmd.scriptpath = cmd_to_script_path(job.cmd)! return job.cmd.scriptpath }