github.com/hy3/cuto@v0.9.8-0.20160830082821-aa6652f877b7/servant/job/job.go (about) 1 // Copyright 2015 unirita Inc. 2 // Created 2015/04/10 shanxia 3 4 package job 5 6 import ( 7 "bufio" 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "strings" 16 "syscall" 17 "time" 18 19 "github.com/unirita/cuto/console" 20 "github.com/unirita/cuto/db" 21 "github.com/unirita/cuto/log" 22 "github.com/unirita/cuto/message" 23 "github.com/unirita/cuto/servant/config" 24 "github.com/unirita/cuto/utctime" 25 ) 26 27 // 実行ジョブ情報 28 type jobInstance struct { 29 config *config.ServantConfig // サーバントの設定情報 30 nID int // ネットワークID 31 path string // ジョブファイル 32 param string // 実行時パラメータ 33 env string // 環境変数 34 workDir string // 作業フォルダ 35 wrnRC int // 警告終了の戻り値 36 wrnPtn string // 警告終了の文字列パターン 37 errRC int // 異常終了の戻り値 38 errPtn string // 異常終了の文字列パターン 39 timeout int // 実行タイムアウトまでの時間(秒) 40 jID string // ジョブID 41 rc int // ジョブの戻り値 42 stat int // ジョブステータス 43 detail string // 異常終了時のメッセージ 44 variable string // 変数情報 45 st string // ジョブ開始日時 46 et string // ジョブ終了日時 47 joblog string // ジョブログ内容 48 joblogFile string // ジョブログファイル名 49 joblogTimestamp string // ジョブログファイル名に使用するタイムスタンプ文字列 50 } 51 52 var ( 53 detailWarnRC = "JOB-RC exceeded MAX-WarnRC." 54 detailErrRC = "JOB-RC exceeded MAX-ErrRC." 55 detailWarnPtn = "JOB-OUTPUT matched Warning Pattern." 56 detailErrPtn = "JOB-OUTPUT matched Error Pattern." 57 ) 58 59 // 実行ジョブ情報のコンストラクタ 60 func newJobInstance(req *message.Request, conf *config.ServantConfig) *jobInstance { 61 job := new(jobInstance) 62 job.config = conf 63 job.nID = req.NID 64 job.path = req.Path 65 job.param = req.Param 66 job.env = req.Env 67 job.workDir = req.Workspace 68 job.wrnRC = req.WarnRC 69 job.wrnPtn = req.WarnStr 70 job.errRC = req.ErrRC 71 job.errPtn = req.ErrStr 72 job.timeout = req.Timeout 73 job.jID = req.JID 74 75 return job 76 } 77 78 // ジョブの実行要求を受け付けて実行する。 79 // 80 // param : req マスタからの要求メッセージ。 81 // 82 // param : conf サーバントの設定情報。 83 // 84 // param : stCh スタート時刻送信用チャンネル 85 // 86 // return : マスタへ返信するメッセージ。 87 func DoJobRequest(req *message.Request, conf *config.ServantConfig, stCh chan<- string) *message.Response { 88 job := newJobInstance(req, conf) 89 if err := job.do(stCh); err != nil { 90 console.DisplayError("CTS019E", err) 91 job.stat = db.ABNORMAL 92 job.detail = err.Error() 93 return job.createResponse() 94 } 95 96 console.Display("CTS011I", job.path, job.nID, job.jID, job.stat, job.rc) 97 job.setVariableValue() 98 return job.createResponse() 99 } 100 101 func (j *jobInstance) do(stCh chan<- string) error { 102 isDockerJob := j.path == message.DockerTag 103 cmd := j.createShell() 104 if isDockerJob && cmd.Path == "" { 105 return errors.New("Cannot execute job on Docker, because docker_command_path is lacked in servant.ini") 106 } 107 108 if err := j.run(cmd, stCh); err != nil { 109 return err 110 } 111 112 if j.config.Job.DisuseJoblog == 0 { 113 if err := j.writeJoblog(); err != nil { 114 return err 115 } 116 } 117 118 // RCからの結果と、出力MSGの結果を比較し、大きい方(異常の方)を採用する 119 rcSt, rcMsg := j.judgeRC() 120 ptnSt, ptnMsg := j.judgeJoblog() 121 if rcSt > ptnSt { 122 j.stat = rcSt 123 j.detail = rcMsg 124 } else { 125 j.stat = ptnSt 126 j.detail = ptnMsg 127 } 128 129 return nil 130 } 131 132 // ジョブファイルの拡張子を確認して、実行シェルを作成します。 133 func (j *jobInstance) createShell() *exec.Cmd { 134 shell, params := j.organizePathAndParam() 135 cmd := exec.Command(shell, params...) 136 137 // 環境変数指定がない場合は、既存の物のみを追加する。 138 if len(j.env) > 0 { 139 envs := strings.Split(j.env, "+") 140 cmd.Env = append(envs, os.Environ()...) 141 } else { 142 cmd.Env = os.Environ() 143 } 144 if len(j.workDir) > 0 { 145 cmd.Dir = j.workDir 146 } else { 147 cmd.Dir = j.config.Dir.JobDir 148 } 149 150 return cmd 151 } 152 153 func (j *jobInstance) organizePathAndParam() (string, []string) { 154 var shell string 155 var params []string 156 if j.path == message.DockerTag { 157 shell = j.config.Job.DockerCommandPath 158 params = paramSplit(j.param) 159 // コンテナ上での実行ファイル名を用いてジョブログが作成されるよう、j.pathを上書き 160 for index, param := range params { 161 if param == "exec" { 162 index += 2 163 if index < len(params) { 164 j.path = params[index] 165 } else { 166 j.path = "" 167 } 168 break 169 } 170 } 171 } else { 172 // ジョブファイル名のみの場合は、デフォルト場所を指定 173 if !filepath.IsAbs(j.path) { 174 j.path = filepath.Join(j.config.Dir.JobDir, j.path) 175 } 176 var paramStr string 177 switch filepath.Ext(j.path) { 178 case ".vbs": 179 fallthrough 180 case ".js": 181 shell = "cscript" 182 paramStr = fmt.Sprintf("/nologo %s %s", shellFormat(j.path), j.param) 183 case ".jar": 184 shell = "java" 185 paramStr = fmt.Sprintf("-jar %s %s", shellFormat(j.path), j.param) 186 case ".ps1": 187 shell = "powershell" 188 if sep := strings.IndexRune(j.path, ' '); sep != -1 { 189 paramStr = fmt.Sprintf("\"& '%s' %s\"", j.path, j.param) 190 } else { 191 paramStr = fmt.Sprintf("%s %s", j.path, j.param) 192 } 193 default: 194 shell = j.path 195 paramStr = j.param 196 } 197 params = paramSplit(paramStr) 198 } 199 200 return shell, params 201 } 202 203 // ジョブ実行を行い、そのリターンコードを返す。 204 func (j *jobInstance) run(cmd *exec.Cmd, stCh chan<- string) error { 205 isJoblogDisabled := j.config.Job.DisuseJoblog != 0 206 outputBuffer := new(bytes.Buffer) 207 208 if isJoblogDisabled { 209 outputWriter := io.MultiWriter(os.Stdout, outputBuffer) 210 cmd.Stdout = outputWriter 211 cmd.Stderr = outputWriter 212 } else { 213 cmd.Stdout = outputBuffer 214 cmd.Stderr = outputBuffer 215 } 216 217 if err := cmd.Start(); err != nil { 218 return err 219 } 220 startTime := utctime.Now() 221 j.st = startTime.String() // ジョブ開始日時の取得 222 j.joblogTimestamp = startTime.FormatLocaltime(utctime.NoDelimiter) 223 stCh <- j.st 224 225 console.Display("CTS010I", j.path, j.nID, j.jID, cmd.Process.Pid) 226 227 err := j.waitCmdTimeout(cmd) 228 j.et = utctime.Now().String() // ジョブ終了日時の取得 229 230 if err != nil { 231 if e2, ok := err.(*exec.ExitError); ok { 232 if s, ok := e2.Sys().(syscall.WaitStatus); ok { 233 j.rc = s.ExitStatus() 234 err = nil 235 } else { 236 j.detail = errors.New("Unimplemented for system where exec.ExitError.Sys() is not syscall.WaitStatus.").Error() 237 } 238 } 239 } else { 240 j.rc = 0 241 } 242 j.joblog = outputBuffer.String() 243 return err 244 } 245 246 func (j *jobInstance) waitCmdTimeout(cmd *exec.Cmd) error { 247 if j.timeout == 0 { 248 // timeoutが0の場合はタイムアウトなしでジョブ終了を待つ 249 return cmd.Wait() 250 } 251 252 ch := make(chan error, 1) 253 go func() { 254 defer close(ch) 255 ch <- cmd.Wait() 256 }() 257 258 t := time.Duration(j.timeout) * time.Second 259 select { 260 case err := <-ch: 261 return err 262 case <-time.After(t): 263 cmd.Process.Kill() 264 return errors.New("Process timeout.") 265 } 266 267 return nil 268 } 269 270 // ジョブのRCを確認し、statを返す。 271 // ジョブのRCが指定されたRC以上の場合は、それぞれのステータスを返します。 272 func (j *jobInstance) judgeRC() (int, string) { 273 if j.errRC > 0 { 274 if j.errRC <= j.rc { 275 return db.ABNORMAL, detailErrRC 276 } 277 } 278 if j.wrnRC > 0 { 279 if j.wrnRC <= j.rc { 280 return db.WARN, detailWarnRC 281 } 282 } 283 return db.NORMAL, "" 284 } 285 286 // ジョブログ結果を確認し、ステータスを返す。 287 func (j *jobInstance) judgeJoblog() (int, string) { 288 if len(j.errPtn) > 0 { 289 if strings.Contains(j.joblog, j.errPtn) { 290 return db.ABNORMAL, detailErrPtn 291 } 292 } 293 if len(j.wrnPtn) > 0 { 294 if strings.Contains(j.joblog, j.wrnPtn) { 295 return db.WARN, detailWarnPtn 296 } 297 } 298 return db.NORMAL, "" 299 } 300 301 // ジョブログ結果を確認し、ステータスを返す。 302 // joblog内に指定された文字列が存在する場合は、それぞれのステータスを返します。 303 func (j *jobInstance) writeJoblog() error { 304 // ジョブログファイル名の作成 305 j.joblogFile = j.createJoblogFileName() 306 log.Debug("joblogFile = ", j.joblogFile) 307 308 // ファイルは存在しない場合の新規作成モード。 309 file, err := os.OpenFile(j.joblogFile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0666) 310 if err != nil { 311 return err 312 } 313 defer file.Close() 314 315 _, err = file.WriteString(j.joblog) 316 return err 317 } 318 319 // ジョブログファイル名をフルパスで作成する。 320 // ”開始日(YYYYMMDD)\インスタンスID.ジョブ名(拡張子なし).開始日時(yyyyMMddHHmmss.sss).log 321 func (j *jobInstance) createJoblogFileName() string { 322 var job string // ジョブ名(拡張子なし)の取得 323 if strings.LastIndex(j.path, "\\") != -1 { 324 tokens := strings.Split(j.path, "\\") 325 job = tokens[len(tokens)-1] 326 } else if strings.LastIndex(j.path, "/") != -1 { 327 tokens := strings.Split(j.path, "/") 328 job = tokens[len(tokens)-1] 329 } else { 330 job = j.path 331 } 332 if extpos := strings.LastIndex(job, "."); extpos != -1 { 333 job = job[:extpos] 334 } 335 // 開始日フォルダの作成 336 joblogDir := filepath.Join(j.config.Dir.JoblogDir, j.joblogTimestamp[:8]) 337 if _, err := os.Stat(joblogDir); err != nil { 338 os.Mkdir(joblogDir, 0777) 339 } 340 log.Debug("joblogDir = ", joblogDir) 341 joblogFileName := fmt.Sprintf("%v.%v.%v.%v.log", j.nID, job, j.jID, j.joblogTimestamp) 342 return filepath.Join(joblogDir, joblogFileName) 343 } 344 345 // レスポンスメッセージの作成 346 func (j *jobInstance) createResponse() *message.Response { 347 var res message.Response 348 res.NID = j.nID 349 res.JID = j.jID 350 res.RC = j.rc 351 res.Stat = j.stat 352 res.Detail = j.detail 353 res.Var = j.variable 354 res.St = j.st 355 res.Et = j.et 356 res.JoblogFile = filepath.Base(j.joblogFile) 357 return &res 358 } 359 360 // ジョブログファイルから変数情報を取得する。 361 func (j *jobInstance) setVariableValue() { 362 reader := strings.NewReader(j.joblog) 363 scanner := bufio.NewScanner(reader) 364 var line string 365 for scanner.Scan() { 366 line = scanner.Text() 367 } 368 j.variable = line 369 }