2022-03-26 13:23:39 +05:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-05-10 19:44:41 +05:00
|
|
|
"database/sql"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-03-29 21:38:04 +05:00
|
|
|
"io"
|
2022-03-26 13:23:39 +05:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/BurntSushi/toml"
|
2022-03-29 21:38:04 +05:00
|
|
|
formatter "github.com/antonfisher/nested-logrus-formatter"
|
2022-03-26 13:23:39 +05:00
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
)
|
|
|
|
|
2022-03-29 19:45:31 +05:00
|
|
|
// JobConfig is a TOML representation of job
|
2022-03-26 13:23:39 +05:00
|
|
|
type JobConfig struct {
|
2022-05-10 19:44:41 +05:00
|
|
|
Type JobType
|
|
|
|
|
|
|
|
// JobType = Cmd
|
|
|
|
Command string // command for execution
|
|
|
|
|
|
|
|
// JobType = Sql
|
|
|
|
Driver string
|
|
|
|
ConnectionString string
|
|
|
|
SqlText string
|
|
|
|
|
|
|
|
// Other fields
|
2022-03-29 21:38:04 +05:00
|
|
|
Cron string // cron decription
|
|
|
|
Description string // job description
|
|
|
|
NumberOfRestartAttemts int
|
|
|
|
RestartSec int // the time to sleep before restarting a job (seconds)
|
|
|
|
RestartRule RestartRule // Configures whether the job shall be restarted when the job process exits
|
2022-06-12 13:34:25 +05:00
|
|
|
|
|
|
|
OnSuccessCmd string
|
|
|
|
OnErrorCmd string
|
2022-03-26 13:23:39 +05:00
|
|
|
}
|
|
|
|
|
2022-03-28 19:55:43 +05:00
|
|
|
type Job struct {
|
|
|
|
Name string // from filename
|
|
|
|
|
2022-03-29 21:38:04 +05:00
|
|
|
JobConfig JobConfig
|
2022-03-28 19:55:43 +05:00
|
|
|
|
|
|
|
// Fields for stats
|
2022-03-30 21:06:55 +05:00
|
|
|
Status Status
|
2022-03-28 19:55:43 +05:00
|
|
|
CurrentRunningCount int
|
|
|
|
LastStartTime string
|
|
|
|
LastEndTime string
|
|
|
|
LastExecutionDuration string
|
|
|
|
LastError string
|
|
|
|
NextLaunch string
|
|
|
|
}
|
|
|
|
|
2022-03-26 20:38:39 +05:00
|
|
|
var globalMutex sync.RWMutex
|
2022-03-26 13:23:39 +05:00
|
|
|
|
|
|
|
func readJob(filePath string) (*Job, error) {
|
|
|
|
var jobConfig JobConfig
|
|
|
|
|
|
|
|
_, err := toml.DecodeFile(filePath, &jobConfig)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-05-10 19:44:41 +05:00
|
|
|
if jobConfig.Type <= 0 {
|
|
|
|
return nil, errors.New("job type is not specified")
|
|
|
|
}
|
|
|
|
|
|
|
|
if jobConfig.Type > 2 {
|
|
|
|
return nil, fmt.Errorf("unknown job type id: %v", int(jobConfig.Type)) // TODO: add job name to log
|
|
|
|
}
|
|
|
|
|
2022-03-26 13:23:39 +05:00
|
|
|
job := &Job{
|
2022-03-29 19:45:31 +05:00
|
|
|
Name: strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)),
|
2022-03-30 21:06:55 +05:00
|
|
|
Status: Inactive,
|
2022-03-29 19:45:31 +05:00
|
|
|
JobConfig: jobConfig}
|
2022-03-26 13:23:39 +05:00
|
|
|
|
|
|
|
return job, nil
|
|
|
|
}
|
|
|
|
|
2022-06-12 13:34:25 +05:00
|
|
|
func splitCommandAndParams(s string) (command string, params []string) {
|
2022-03-29 19:45:31 +05:00
|
|
|
quoted := false
|
2022-06-12 13:34:25 +05:00
|
|
|
items := strings.FieldsFunc(s, func(r rune) bool {
|
2022-03-29 19:45:31 +05:00
|
|
|
if r == '"' {
|
|
|
|
quoted = !quoted
|
|
|
|
}
|
|
|
|
return !quoted && r == ' '
|
|
|
|
})
|
|
|
|
for i := range items {
|
|
|
|
items[i] = strings.Trim(items[i], `"`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return items[0], items[1:]
|
|
|
|
}
|
|
|
|
|
2022-03-29 21:52:45 +05:00
|
|
|
// logEntry - logger which merged with main logger,
|
|
|
|
// jobLogFile - job log file with needs to be closed after job is done
|
|
|
|
func (j *Job) openAndMergeLog() (logEntry *log.Entry, jobLogFile *os.File) {
|
|
|
|
jobLogFile, _ = os.OpenFile(filepath.Join(config.LogFilesPath, j.Name+".log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) // TODO: handle error
|
2022-03-29 21:38:04 +05:00
|
|
|
jobLogFile.WriteString("\n")
|
2022-03-26 13:23:39 +05:00
|
|
|
|
2022-05-11 18:59:33 +05:00
|
|
|
logWriter := io.MultiWriter(mainLogFile, jobLogFile)
|
2022-03-26 13:23:39 +05:00
|
|
|
|
2022-03-29 21:38:04 +05:00
|
|
|
log := log.New()
|
|
|
|
log.SetFormatter(&formatter.Formatter{
|
|
|
|
TimestampFormat: config.TimeFormat,
|
|
|
|
HideKeys: true,
|
|
|
|
NoColors: true,
|
|
|
|
TrimMessages: true})
|
|
|
|
log.SetOutput(logWriter)
|
2022-03-29 21:52:45 +05:00
|
|
|
logEntry = log.WithField("job", j.Name)
|
|
|
|
|
|
|
|
return logEntry, jobLogFile
|
|
|
|
}
|
|
|
|
|
|
|
|
func (j *Job) Run() {
|
|
|
|
log, jobLogFile := j.openAndMergeLog()
|
|
|
|
defer jobLogFile.Close()
|
2022-03-26 13:23:39 +05:00
|
|
|
|
2022-03-29 21:38:04 +05:00
|
|
|
for i := 0; i < j.JobConfig.NumberOfRestartAttemts+1; i++ {
|
2022-05-10 21:36:34 +05:00
|
|
|
err := j.runTry(log, jobLogFile)
|
2022-03-29 21:38:04 +05:00
|
|
|
if err == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if j.JobConfig.RestartRule == No || j.JobConfig.NumberOfRestartAttemts == 0 {
|
|
|
|
break
|
|
|
|
}
|
2022-03-26 13:23:39 +05:00
|
|
|
|
2022-03-29 21:38:04 +05:00
|
|
|
if i == 0 {
|
2022-03-29 21:52:45 +05:00
|
|
|
log.Printf("Job failed, restarting in %d seconds.", j.JobConfig.RestartSec)
|
2022-03-30 21:06:55 +05:00
|
|
|
j.Status = Restarting
|
|
|
|
} else if i < j.JobConfig.NumberOfRestartAttemts {
|
|
|
|
j.Status = Restarting
|
2022-03-29 21:52:45 +05:00
|
|
|
log.Printf("Retry attempt №%d of %d failed, restarting in %d seconds.", i, j.JobConfig.NumberOfRestartAttemts, j.JobConfig.RestartSec)
|
2022-03-29 21:38:04 +05:00
|
|
|
} else {
|
2022-03-29 21:52:45 +05:00
|
|
|
log.Printf("Retry attempt №%d of %d failed.", i, j.JobConfig.NumberOfRestartAttemts)
|
2022-03-29 21:38:04 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(time.Duration(j.JobConfig.RestartSec) * time.Second)
|
|
|
|
}
|
2022-03-26 13:23:39 +05:00
|
|
|
}
|
2022-05-10 19:44:41 +05:00
|
|
|
|
2022-05-10 21:36:34 +05:00
|
|
|
func (j *Job) runTry(log *log.Entry, jobLogFile *os.File) error {
|
|
|
|
log.Info("Started.")
|
|
|
|
startTime := time.Now()
|
|
|
|
|
|
|
|
globalMutex.Lock()
|
|
|
|
j.CurrentRunningCount++
|
|
|
|
j.Status = Running
|
|
|
|
j.LastStartTime = startTime.Format(config.TimeFormat)
|
|
|
|
globalMutex.Unlock()
|
|
|
|
|
|
|
|
var err error
|
|
|
|
switch j.JobConfig.Type {
|
|
|
|
case Cmd:
|
|
|
|
err = j.runCmd(jobLogFile)
|
|
|
|
case Sql:
|
|
|
|
err = j.runSql(jobLogFile)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
j.Status = Error
|
|
|
|
log.Error(err.Error())
|
|
|
|
|
|
|
|
globalMutex.Lock()
|
|
|
|
j.LastError = err.Error()
|
|
|
|
globalMutex.Unlock()
|
|
|
|
} else {
|
|
|
|
j.Status = Inactive
|
|
|
|
globalMutex.Lock()
|
|
|
|
j.LastError = ""
|
|
|
|
globalMutex.Unlock()
|
|
|
|
}
|
2022-06-12 13:34:25 +05:00
|
|
|
go j.runFinishCallback(err)
|
2022-05-10 21:36:34 +05:00
|
|
|
|
|
|
|
endTime := time.Now()
|
|
|
|
log.Infof("Finished (%s).", endTime.Sub(startTime).Truncate(time.Second).String())
|
|
|
|
|
|
|
|
globalMutex.Lock()
|
|
|
|
j.CurrentRunningCount--
|
|
|
|
j.LastEndTime = endTime.Format(config.TimeFormat)
|
|
|
|
j.LastExecutionDuration = endTime.Sub(startTime).Truncate(time.Second).String()
|
|
|
|
globalMutex.Unlock()
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-05-10 19:44:41 +05:00
|
|
|
func (j *Job) runCmd(jobLogFile *os.File) error {
|
2022-06-12 13:34:25 +05:00
|
|
|
command, params := splitCommandAndParams(j.JobConfig.Command)
|
2022-05-10 19:44:41 +05:00
|
|
|
|
|
|
|
cmd := exec.Command(command, params...)
|
|
|
|
cmd.Stdout = jobLogFile
|
|
|
|
cmd.Stderr = jobLogFile
|
|
|
|
|
|
|
|
return cmd.Run()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (j *Job) runSql(jobLogFile *os.File) error {
|
2022-05-14 14:17:52 +05:00
|
|
|
var (
|
|
|
|
db *sql.DB
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
switch j.JobConfig.Driver {
|
|
|
|
case "mssql", "sqlserver":
|
|
|
|
db, err = openMsSqlDb(j.JobConfig.ConnectionString, jobLogFile)
|
|
|
|
default:
|
|
|
|
db, err = sql.Open(j.JobConfig.Driver, j.JobConfig.ConnectionString)
|
|
|
|
}
|
2022-05-10 19:44:41 +05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
_, err = db.Exec(j.JobConfig.SqlText)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2022-06-12 13:34:25 +05:00
|
|
|
|
|
|
|
func (j *Job) runFinishCallback(err error) error {
|
|
|
|
s := j.JobConfig.OnSuccessCmd
|
|
|
|
|
|
|
|
// Fill variables
|
|
|
|
errStr := ""
|
|
|
|
if err != nil {
|
|
|
|
errStr = err.Error()
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
s = strings.ReplaceAll(s, "$ErrorText", errStr)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err == nil && j.JobConfig.OnSuccessCmd != "" {
|
|
|
|
log.Println("success")
|
|
|
|
cmd, params := splitCommandAndParams(s)
|
|
|
|
return runSimpleCmd(cmd, params...)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil && j.JobConfig.OnErrorCmd != "" {
|
|
|
|
log.Println("error")
|
|
|
|
cmd, params := splitCommandAndParams(s)
|
|
|
|
return runSimpleCmd(cmd, params...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func runSimpleCmd(command string, args ...string) error {
|
|
|
|
log.Println(command)
|
|
|
|
log.Println(args)
|
|
|
|
err := exec.Command(command, args...).Run()
|
|
|
|
if err != nil {
|
|
|
|
log.Println(">>", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|