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  }