支持后台手动停止运行中的shell任务

pull/21/merge
ouqiang 2018-01-27 11:10:08 +08:00
parent fd39434446
commit f081d89fb4
12 changed files with 108 additions and 68 deletions

View File

@ -2,11 +2,11 @@
# set -x -u # set -x -u
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz # 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
# ./build.sh -p windows -a amd64 # ./build.sh -p windows -a amd64 -v 1.4
# 参数含义 # 参数含义
# -p 指定平台(windows|linux|darwin) # -p 指定平台(windows|linux|darwin)
# -a 指定体系架构(amd64|386), 默认amd64 # -a 指定体系架构(amd64|386), 默认amd64
# -v 版本号
TEMP_DIR=`date +%s`-temp-`echo $RANDOM` TEMP_DIR=`date +%s`-temp-`echo $RANDOM`

View File

@ -2,10 +2,11 @@
# set -x -u # set -x -u
# 任务节点打包, 生成压缩包 gocron-node.zip或gocron-node.tar.gz # 任务节点打包, 生成压缩包 gocron-node.zip或gocron-node.tar.gz
# ./build-node.sh -p windows -a amd64 # ./build-node.sh -p windows -a amd64 -v 1.4
# 参数含义 # 参数含义
# -p 指定平台(windows|linux|darwin) # -p 指定平台(windows|linux|darwin)
# -a 指定体系架构(amd64|386), 默认amd64 # -a 指定体系架构(amd64|386), 默认amd64
# -v 版本号
# 目标平台 windows,linux,darwin # 目标平台 windows,linux,darwin

View File

@ -150,7 +150,7 @@ func shutdown() {
serviceTask := new(service.Task) serviceTask := new(service.Task)
// 停止所有任务调度 // 停止所有任务调度
logger.Info("停止定时任务调度") logger.Info("停止定时任务调度")
serviceTask.Stop() serviceTask.WaitAndExit()
} }
// 判断应用是否需要升级, 当存在版本号文件且版本小于app.VersionId时升级 // 判断应用是否需要升级, 当存在版本号文件且版本小于app.VersionId时升级

View File

@ -36,7 +36,7 @@ func InitEnv(versionString string) {
DataDir = AppDir + "/data" DataDir = AppDir + "/data"
AppConfig = ConfDir + "/app.ini" AppConfig = ConfDir + "/app.ini"
VersionFile = ConfDir + "/.version" VersionFile = ConfDir + "/.version"
createDirIfNeed(ConfDir, LogDir, DataDir) createDirIfNotExists(ConfDir, LogDir, DataDir)
Installed = IsInstalled() Installed = IsInstalled()
VersionId = ToNumberVersion(versionString) VersionId = ToNumberVersion(versionString)
} }
@ -108,7 +108,7 @@ func ToNumberVersion(versionString string) int {
} }
// 检测目录是否存在 // 检测目录是否存在
func createDirIfNeed(path ...string) { func createDirIfNotExists(path ...string) {
for _, value := range path { for _, value := range path {
if !utils.FileExist(value) { if !utils.FileExist(value) {
err := os.Mkdir(value, 0755) err := os.Mkdir(value, 0755)

View File

@ -10,12 +10,30 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"time" "time"
"sync"
)
var (
taskMap sync.Map
) )
var ( var (
errUnavailable = errors.New("无法连接远程服务器") errUnavailable = errors.New("无法连接远程服务器")
) )
func generateTaskUniqueKey(ip string, port int, id int64) string {
return fmt.Sprintf("%s:%d:%d", ip, port, id)
}
func Stop(ip string, port int , id int64) {
key := generateTaskUniqueKey(ip, port, id)
cancel, ok := taskMap.Load(key)
if !ok {
return
}
cancel.(context.CancelFunc)()
}
func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) { func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
@ -39,7 +57,9 @@ func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
} }
timeout := time.Duration(taskReq.Timeout) * time.Second timeout := time.Duration(taskReq.Timeout) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() taskUniqueKey := generateTaskUniqueKey(ip, port, taskReq.Id)
taskMap.Store(taskUniqueKey, cancel)
defer taskMap.Delete(taskUniqueKey)
resp, err := c.Run(ctx, taskReq) resp, err := c.Run(ctx, taskReq)
if err != nil { if err != nil {
return parseGRPCError(err, conn, &isConnClosed) return parseGRPCError(err, conn, &isConnClosed)
@ -60,6 +80,8 @@ func parseGRPCError(err error, conn *grpc.ClientConn, connClosed *bool) (string,
return "", errUnavailable return "", errUnavailable
case codes.DeadlineExceeded: case codes.DeadlineExceeded:
return "", errors.New("执行超时, 强制结束") return "", errors.New("执行超时, 强制结束")
case codes.Canceled:
return "", errors.New("手动停止")
} }
return "", err return "", err
} }

View File

@ -36,6 +36,7 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type TaskRequest struct { type TaskRequest struct {
Command string `protobuf:"bytes,2,opt,name=command" json:"command,omitempty"` Command string `protobuf:"bytes,2,opt,name=command" json:"command,omitempty"`
Timeout int32 `protobuf:"varint,3,opt,name=timeout" json:"timeout,omitempty"` Timeout int32 `protobuf:"varint,3,opt,name=timeout" json:"timeout,omitempty"`
Id int64 `protobuf:"varint,4,opt,name=id" json:"id,omitempty"`
} }
func (m *TaskRequest) Reset() { *m = TaskRequest{} } func (m *TaskRequest) Reset() { *m = TaskRequest{} }
@ -57,6 +58,13 @@ func (m *TaskRequest) GetTimeout() int32 {
return 0 return 0
} }
func (m *TaskRequest) GetId() int64 {
if m != nil {
return m.Id
}
return 0
}
type TaskResponse struct { type TaskResponse struct {
Output string `protobuf:"bytes,1,opt,name=output" json:"output,omitempty"` Output string `protobuf:"bytes,1,opt,name=output" json:"output,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"` Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
@ -161,16 +169,17 @@ var _Task_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("task.proto", fileDescriptor0) } func init() { proto.RegisterFile("task.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{ var fileDescriptor0 = []byte{
// 170 bytes of a gzipped FileDescriptorProto // 184 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xb1, 0x0e, 0x82, 0x40, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xcf, 0x8a, 0x83, 0x30,
0x0c, 0x86, 0x45, 0x04, 0x63, 0x75, 0xd0, 0xc6, 0x98, 0x8b, 0x13, 0x61, 0x62, 0x30, 0x0c, 0xea, 0x10, 0xc6, 0x37, 0x46, 0x5d, 0x76, 0x76, 0x59, 0xda, 0xa1, 0x94, 0xd0, 0x93, 0x78, 0xf2, 0x50,
0xe8, 0xe2, 0x2b, 0x5c, 0x7c, 0x01, 0xc4, 0x1b, 0x0c, 0x81, 0x9e, 0xbd, 0xde, 0xfb, 0x1b, 0x38, 0x3c, 0xb4, 0x3d, 0xf6, 0x25, 0x1a, 0xfa, 0x02, 0x56, 0x73, 0x10, 0xd1, 0x49, 0x93, 0xc9, 0xfb,
0x48, 0x18, 0xbf, 0x36, 0xfd, 0xfa, 0xff, 0x00, 0x52, 0xb9, 0xa6, 0xb4, 0x4c, 0x42, 0x18, 0xb3, 0x17, 0xff, 0x81, 0xc7, 0xdf, 0x0c, 0xf3, 0xfd, 0xe6, 0x03, 0xe0, 0xca, 0x77, 0xa5, 0x75, 0xc4,
0xad, 0xf3, 0x27, 0x6c, 0x5f, 0x95, 0x6b, 0xb4, 0xf9, 0x79, 0xe3, 0x04, 0x15, 0xac, 0x6b, 0x6a, 0x84, 0xd2, 0xd9, 0x3a, 0x7f, 0xc0, 0xef, 0xb3, 0xf2, 0x9d, 0x36, 0xef, 0x60, 0x3c, 0xa3, 0x82,
0xdb, 0xaa, 0xfb, 0xa8, 0x65, 0x16, 0x15, 0x1b, 0x3d, 0x61, 0xbf, 0x91, 0x6f, 0x6b, 0xc8, 0x8b, 0xef, 0x9a, 0xfa, 0xbe, 0x1a, 0x1a, 0x15, 0x65, 0xa2, 0xf8, 0xd1, 0x2b, 0x8e, 0x1b, 0x6e, 0x7b,
0x8a, 0xb3, 0xa8, 0x48, 0xf4, 0x84, 0xf9, 0x03, 0x76, 0x41, 0xe1, 0x2c, 0x75, 0xce, 0xe0, 0x09, 0x43, 0x81, 0x95, 0xcc, 0x44, 0x91, 0xe8, 0x15, 0xf1, 0x1f, 0xa2, 0xb6, 0x51, 0x71, 0x26, 0x0a,
0x52, 0xf2, 0x62, 0xbd, 0xa8, 0x68, 0x50, 0x8c, 0x84, 0x47, 0x48, 0x0c, 0x33, 0xf1, 0x68, 0x0e, 0xa9, 0xa3, 0xb6, 0xc9, 0xef, 0xf0, 0x37, 0x47, 0x7a, 0x4b, 0x83, 0x37, 0x78, 0x84, 0x94, 0x02,
0x70, 0xbd, 0xc3, 0xaa, 0xbf, 0xc6, 0x0b, 0xc4, 0xda, 0x77, 0xb8, 0x2f, 0xd9, 0xd6, 0xe5, 0x2c, 0xdb, 0xc0, 0x4a, 0x4c, 0x91, 0x0b, 0xe1, 0x01, 0x12, 0xe3, 0x1c, 0xb9, 0xc5, 0x34, 0xc3, 0xe5,
0xd2, 0xf9, 0x30, 0x9b, 0x84, 0x0f, 0xf9, 0xe2, 0x9d, 0x0e, 0x15, 0x6e, 0xff, 0x00, 0x00, 0x00, 0x06, 0xf1, 0x78, 0x8d, 0x67, 0x90, 0x3a, 0x0c, 0xb8, 0x2b, 0x9d, 0xad, 0xcb, 0xcd, 0x8b, 0xa7,
0xff, 0xff, 0xef, 0x3c, 0x71, 0x6b, 0xd0, 0x00, 0x00, 0x00, 0xfd, 0x66, 0x32, 0x1b, 0xf2, 0xaf, 0x57, 0x3a, 0x55, 0xba, 0x7e, 0x02, 0x00, 0x00, 0xff, 0xff,
0xd7, 0x7f, 0x8a, 0x9d, 0xe0, 0x00, 0x00, 0x00,
} }

View File

@ -9,6 +9,7 @@ service Task {
message TaskRequest { message TaskRequest {
string command = 2; // string command = 2; //
int32 timeout = 3; // int32 timeout = 3; //
int64 id = 4; // ID
} }
message TaskResponse { message TaskResponse {

View File

@ -66,6 +66,7 @@ func Register(m *macaron.Macaron) {
m.Get("", task.Index) m.Get("", task.Index)
m.Get("/log", tasklog.Index) m.Get("/log", tasklog.Index)
m.Post("/log/clear", tasklog.Clear) m.Post("/log/clear", tasklog.Clear)
m.Post("/log/stop", tasklog.Stop)
m.Post("/remove/:id", task.Remove) m.Post("/remove/:id", task.Remove)
m.Post("/enable/:id", task.Enable) m.Post("/enable/:id", task.Enable)
m.Post("/disable/:id", task.Disable) m.Post("/disable/:id", task.Disable)

View File

@ -11,6 +11,7 @@ import (
"github.com/ouqiang/gocron/routers/base" "github.com/ouqiang/gocron/routers/base"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
"html/template" "html/template"
"github.com/ouqiang/gocron/service"
) )
func Index(ctx *macaron.Context) { func Index(ctx *macaron.Context) {
@ -48,6 +49,31 @@ func Clear(ctx *macaron.Context) string {
return json.Success(utils.SuccessContent, nil) return json.Success(utils.SuccessContent, nil)
} }
// 停止运行中的任务
func Stop(ctx *macaron.Context) string {
id := ctx.QueryInt64("id")
taskId := ctx.QueryInt("task_id")
taskModel := new(models.Task)
task, err := taskModel.Detail(taskId)
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure("获取任务信息失败#" + err.Error(), err)
}
if task.Protocol != models.TaskRPC {
return json.CommonFailure("仅支持SHELL任务手动停止")
}
if len(task.Hosts) == 0 {
return json.CommonFailure("任务节点列表为空")
}
serviceTask := new(service.Task)
for _, host := range task.Hosts {
serviceTask.Stop(host.Name, host.Port, id)
}
return json.Success("已执行停止操作, 请等待任务退出", nil);
}
// 删除N个月前的日志 // 删除N个月前的日志
func Remove(ctx *macaron.Context) string { func Remove(ctx *macaron.Context) string {
month := ctx.ParamsInt(":id") month := ctx.ParamsInt(":id")

View File

@ -14,6 +14,7 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"net/http"
) )
// 定时任务调度管理器 // 定时任务调度管理器
@ -127,8 +128,13 @@ func (task *Task) Add(taskModel models.Task) {
} }
} }
// 停止所有任务 // 停止运行中的任务
func (task *Task) Stop() { func (task *Task) Stop(ip string, port int, id int64) {
rpcClient.Stop(ip, port, id)
}
// 等待所有任务结束后退出
func (task *Task) WaitAndExit() {
Cron.Stop() Cron.Stop()
taskCount.Exit() taskCount.Exit()
} }
@ -139,7 +145,7 @@ func (task *Task) Run(taskModel models.Task) {
} }
type Handler interface { type Handler interface {
Run(taskModel models.Task) (string, error) Run(taskModel models.Task, taskUniqueId int64) (string, error)
} }
// HTTP任务 // HTTP任务
@ -148,13 +154,13 @@ type HTTPHandler struct{}
// http任务执行时间不超过300秒 // http任务执行时间不超过300秒
const HttpExecTimeout = 300 const HttpExecTimeout = 300
func (h *HTTPHandler) Run(taskModel models.Task) (result string, err error) { func (h *HTTPHandler) Run(taskModel models.Task, taskUniqueId int64) (result string, err error) {
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout { if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
taskModel.Timeout = HttpExecTimeout taskModel.Timeout = HttpExecTimeout
} }
resp := httpclient.Get(taskModel.Command, taskModel.Timeout) resp := httpclient.Get(taskModel.Command, taskModel.Timeout)
// 返回状态码非200均为失败 // 返回状态码非200均为失败
if resp.StatusCode != 200 { if resp.StatusCode != http.StatusOK {
return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode)) return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode))
} }
@ -164,10 +170,11 @@ func (h *HTTPHandler) Run(taskModel models.Task) (result string, err error) {
// RPC调用执行任务 // RPC调用执行任务
type RPCHandler struct{} type RPCHandler struct{}
func (h *RPCHandler) Run(taskModel models.Task) (result string, err error) { func (h *RPCHandler) Run(taskModel models.Task, taskUniqueId int64) (result string, err error) {
taskRequest := new(pb.TaskRequest) taskRequest := new(pb.TaskRequest)
taskRequest.Timeout = int32(taskModel.Timeout) taskRequest.Timeout = int32(taskModel.Timeout)
taskRequest.Command = taskModel.Command taskRequest.Command = taskModel.Command
taskRequest.Id = taskUniqueId
var resultChan chan TaskResult = make(chan TaskResult, len(taskModel.Hosts)) var resultChan chan TaskResult = make(chan TaskResult, len(taskModel.Hosts))
for _, taskHost := range taskModel.Hosts { for _, taskHost := range taskModel.Hosts {
go func(th models.TaskHostDetail) { go func(th models.TaskHostDetail) {
@ -250,7 +257,7 @@ func createJob(taskModel models.Task) cron.FuncJob {
return return
} }
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Name, taskModel.Command) logger.Infof("开始执行任务#%s#命令-%s", taskModel.Name, taskModel.Command)
taskResult := execJob(handler, taskModel) taskResult := execJob(handler, taskModel, taskLogId)
logger.Infof("任务完成#%s#命令-%s", taskModel.Name, taskModel.Command) logger.Infof("任务完成#%s#命令-%s", taskModel.Name, taskModel.Command)
afterExecJob(taskModel, taskResult, taskLogId) afterExecJob(taskModel, taskResult, taskLogId)
} }
@ -371,7 +378,7 @@ func SendNotification(taskModel models.Task, taskResult TaskResult) {
} }
// 执行具体任务 // 执行具体任务
func execJob(handler Handler, taskModel models.Task) TaskResult { func execJob(handler Handler, taskModel models.Task, taskUniqueId int64) TaskResult {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
logger.Error("panic#service/task.go:execJob#", err) logger.Error("panic#service/task.go:execJob#", err)
@ -389,7 +396,7 @@ func execJob(handler Handler, taskModel models.Task) TaskResult {
var output string var output string
var err error var err error
for i < execTimes { for i < execTimes {
output, err = handler.Run(taskModel) output, err = handler.Run(taskModel, taskUniqueId)
if err == nil { if err == nil {
return TaskResult{Result: output, Err: err, RetryTimes: i} return TaskResult{Result: output, Err: err, RetryTimes: i}
} }

View File

@ -100,6 +100,11 @@
> >
</button> </button>
{{{end}}} {{{end}}}
{{{if and (eq .Status 1) (eq .Protocol 2) }}}
<button class="ui small blue button" onclick="stopTask({{{.Id}}}, {{{.TaskId}}})"></button>
{{{end}}}
</td> </td>
</tr> </tr>
{{{end}}} {{{end}}}
@ -148,6 +153,14 @@
}).modal('refresh').modal('show'); }).modal('refresh').modal('show');
} }
function stopTask(id, taskId) {
util.confirm("确定要停止任务吗", function () {
util.post("/task/log/stop/", {id: id, task_id:taskId}, function () {
location.reload();
});
});
}
function clearLog() { function clearLog() {
util.confirm("确定要删除所有日志吗?", function() { util.confirm("确定要删除所有日志吗?", function() {
util.post("/task/log/clear",{}, function() { util.post("/task/log/clear",{}, function() {

View File

@ -1,40 +0,0 @@
#!/usr/bin/env bash
# set -x -u
# 上传二进制包到七牛
if [[ -z $QINIU_ACCESS_KEY || -z $QINIU_SECRET_KEY || -z $QINIU_URL ]];then
echo 'QINIU_ACCESS_KEY | QINIU_SECRET_KEY | QINIU_URL is need'
exit 1
fi
# 打包
for i in linux darwin windows
do
./build.sh -p $i
if [[ $? != 0 ]];then
break
fi
./build_node.sh -p $i
if [[ $? != 0 ]];then
break
fi
done
# 身份认证
qrsctl login $QINIU_ACCESS_KEY $QINIU_SECRET_KEY
# 上传
for i in `ls gocron*.gz gocron*.zip`
do
# 上传文件 qrsctl put bucket key srcFile
KEY=gocron/$i
qrsctl put github $KEY $i
if [[ $? != 0 ]];then
break
fi
echo "刷新七牛CDN-" $QINIU_URL/$KEY
qrsctl cdn/refresh $QINIU_URL/$KEY
rm $i
done
echo '打包并上传成功'