Compare commits

..
8 Commits
Author SHA1 Message Date
ouqiang 94197e0511 增加gocron命令行管理工具 2017-05-20 16:26:57 +08:00
ouqiang 519c6613c7 生成windows二进制文件, 隐藏窗口, 后台运行 2017-05-20 14:45:28 +08:00
ouqiang 4a77958556 增加以守护进程模式运行 2017-05-20 14:37:40 +08:00
ouqiang 95278d373e 生产环境访问日志写入文件中 2017-05-20 14:13:16 +08:00
ouqiang d91ae4fa44 更新截图 2017-05-18 15:26:17 +08:00
ouqiang 5e8f2da70e 任务列表改为table布局显示 2017-05-18 12:00:55 +08:00
ouqiang 8779b3ad8b 生成二进制文件,去掉调试信息
bugfix#生成压缩包脚本, 当编译失败时, 未及时退出,而是继续往下执行.
2017-05-17 13:34:44 +08:00
ouqiang 28eda835c0 时间轮所有任务共用一个回调函数 2017-05-17 11:49:54 +08:00
12 changed files with 261 additions and 71 deletions
+9 -4
View File
@@ -48,11 +48,16 @@
3. 编译 `go build`
4. 启动、访问方式同上
### 启动可选参数
### 命令
* -p 端口, 指定端口, 默认5920
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
* -h 查看帮助
* gocron web
* -p 端口, 指定端口, 默认5920
* -e 指定运行环境, dev|test|prod, dev模式下可查看更多日志信息, 默认prod
* -d 后台运行
* -h 查看帮助
* gocron serv
* -s stop|status stop:停止gocron status:查看运行状态
## 安全
* 使用`https`访问保证数据传输安全, 可在web服务器如nginx中配置https,通过反向代理,访问内部的gocron
+8 -3
View File
@@ -53,8 +53,13 @@ if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then
fi
echo '开始编译'
GOOS=$OS GOARCH=$ARCH go build
if [[ ! $? ]];then
if [[ $OS = 'windows' ]];then
GOOS=$OS GOARCH=$ARCH go build -ldflags '-w -H windowsgui'
else
GOOS=$OS GOARCH=$ARCH go build -ldflags '-w'
fi
if [[ $? != 0 ]];then
exit 1
fi
echo '编译完成'
@@ -68,7 +73,7 @@ else
fi
mkdir -p $TEMP_DIR/$APP_NAME
if [[ ! $? ]]; then
if [[ $? != 0 ]]; then
exit 1
fi
+67
View File
@@ -0,0 +1,67 @@
package cmd
import (
"github.com/urfave/cli"
"fmt"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/modules/app"
"os"
"syscall"
)
var CmdServ = cli.Command{
Name: "serv",
Usage: "manage gocron, ./gocron serv -s stop|status",
Action: runServ,
Flags: []cli.Flag{
cli.StringFlag{
Name: "s",
Value:"",
Usage: "stop|status",
},
},
}
func runServ(ctx *cli.Context) {
if utils.IsWindows() {
fmt.Println("not support on windows")
return
}
option := ctx.String("s")
if !utils.InStringSlice([]string{"stop", "status"}, option) {
fmt.Println("invalid option")
return
}
app.InitEnv()
pid := app.GetPid()
if pid <= 0 {
fmt.Println("not running")
return
}
process ,err := os.FindProcess(pid)
if err != nil {
fmt.Println("not running", err)
return
}
switch option {
case "stop":
stop(process)
case "status":
status(process)
}
}
func stop(process *os.Process) {
fmt.Println("stopping gocron......")
err := process.Signal(syscall.SIGTERM)
if err != nil {
fmt.Println("failed to kill process", err)
} else {
fmt.Println("stopped")
}
}
func status(process *os.Process) {
fmt.Printf("running, pid-[%d]\n", process.Pid)
}
+66 -7
View File
@@ -13,48 +13,88 @@ import (
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/setting"
"time"
"io"
"fmt"
"path/filepath"
"os/exec"
"github.com/ouqiang/gocron/modules/utils"
)
// web服务器默认端口
const DefaultPort = 5920
const InitProcess = 1
var CmdWeb = cli.Command{
Name: "web",
Usage: "run web server",
Action: run,
Action: runWeb,
Flags: []cli.Flag{
cli.IntFlag{
Name: "port,p",
Value: DefaultPort,
Usage: "bind port number",
Usage: "bind port",
},
cli.StringFlag{
Name: "env,e",
Value: "prod",
Usage: "runtime environment, dev|test|prod",
},
cli.BoolFlag{
Name: "d",
Usage: "-d=true, run as daemon process",
},
},
}
func run(ctx *cli.Context) {
func runWeb(ctx *cli.Context) {
// 设置守护进程
becomeDaemon(ctx)
// 设置运行环境
setEnvironment(ctx)
// 初始化应用
app.InitEnv()
app.WritePid()
// 初始化模块 DB、定时任务等
initModule()
// 捕捉信号,配置热更新等
go catchSignal()
m := macaron.Classic()
m := macaron.NewWithLogger(getWebLogWriter())
// 注册路由
routers.Register(m)
// 注册中间件.
routers.RegisterMiddleware(m)
port := parsePort(ctx)
fmt.Println("server start")
m.Run(port)
}
func becomeDaemon(ctx *cli.Context) {
// 不支持windows
if utils.IsWindows() {
return
}
if !ctx.IsSet("d") {
return
}
if os.Getppid() == InitProcess {
// 子进程不再处理
return
}
filePath, _:= filepath.Abs(os.Args[0])
cmd := exec.Command(filePath, os.Args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Start()
// 父进程退出, 子进程由init-1号进程收养
os.Exit(0)
}
func initModule() {
if !app.Installed {
return
@@ -140,9 +180,30 @@ func catchSignal() {
}
}
func getWebLogWriter() io.Writer {
if macaron.Env == macaron.DEV {
return os.Stdout
}
logFile := app.LogDir + "/access.log"
var err error
w, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND ,0666)
if err != nil {
fmt.Printf("日志文件[%s]打开失败", logFile)
panic(err)
}
return w
}
// 应用退出
func shutdown() {
if !app.Installed {
defer func() {
app.RemovePid()
logger.Info("已退出")
os.Exit(0)
}()
if !app.Installed {
return
}
logger.Info("应用准备退出")
@@ -168,6 +229,4 @@ func shutdown() {
time.Sleep(3 * time.Second)
taskNumInRunning = service.TaskNum.Num()
}
logger.Info("已退出")
os.Exit(0)
}
+2 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/ouqiang/gocron/cmd"
)
const AppVersion = "0.2"
const AppVersion = "0.3"
func main() {
app := cli.NewApp()
@@ -22,6 +22,7 @@ func main() {
app.Version = AppVersion
app.Commands = []cli.Command{
cmd.CmdWeb,
cmd.CmdServ,
}
app.Flags = append(app.Flags, []cli.Flag{}...)
app.Run(os.Args)
+32
View File
@@ -7,6 +7,8 @@ import (
"runtime"
"github.com/ouqiang/gocron/modules/utils"
"gopkg.in/ini.v1"
"io/ioutil"
"strconv"
)
var (
@@ -17,8 +19,10 @@ var (
AppConfig string // 应用配置文件
Installed bool // 应用是否安装过
Setting *ini.Section // 应用配置
PidFile string
)
func InitEnv() {
runtime.GOMAXPROCS(runtime.NumCPU())
logger.InitLogger()
@@ -31,6 +35,7 @@ func InitEnv() {
LogDir = AppDir + "/log"
DataDir = AppDir + "/data"
AppConfig = ConfDir + "/app.ini"
PidFile = LogDir + "/gocron.pid"
checkDirExists(ConfDir, LogDir, DataDir)
Installed = IsInstalled()
}
@@ -45,6 +50,33 @@ func IsInstalled() bool {
return true
}
func WritePid() {
pid := os.Getpid()
pidStr := strconv.Itoa(pid)
err := ioutil.WriteFile(PidFile, []byte(pidStr), 0644)
if err != nil {
logger.Fatalf("写入pid文件失败", err)
}
}
func GetPid() int {
bytes, err := ioutil.ReadFile(PidFile)
if err != nil {
return 0
}
pidStr := string(bytes)
pid, err := strconv.Atoi(pidStr)
if err != nil {
return 0
}
return pid
}
func RemovePid() {
os.Remove(PidFile)
}
// 创建安装锁文件
func CreateInstallLock() error {
_, err := os.Create(ConfDir + "/install.lock")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 108 KiB

+16 -6
View File
@@ -16,7 +16,7 @@ type DelayTask struct {}
// 从数据库中取出所有延迟任务
func (task *DelayTask) Initialize(tick time.Duration, slots int) {
tw = timewheel.New(tick, slots)
tw = timewheel.New(tick, slots, task.Run)
tw.Start()
taskModel := new(models.DelayTask)
currentTime := time.Now()
@@ -57,18 +57,28 @@ func (task *DelayTask) Add(taskModel models.DelayTask) {
currentTimestamp := time.Now().Unix()
execTimestamp := taskModel.Created.Unix() + int64(taskModel.Delay)
// 时间过期, 立即执行任务
data := []interface{}{taskModel.Id, taskModel.Url, taskModel.Params}
if execTimestamp <= currentTimestamp {
go task.Run(taskModel.Id, taskModel.Url, taskModel.Params)
go task.Run(data)
return
}
delay := execTimestamp - currentTimestamp
tw.Add(time.Duration(delay) * time.Second, func() {
task.Run(taskModel.Id, taskModel.Url, taskModel.Params)
})
tw.Add(time.Duration(delay) * time.Second, data)
}
// 运行任务
func (task *DelayTask) Run(id int64, url, params string) {
func (task *DelayTask) Run(data []interface{}) {
if len(data) < 3 {
logger.Errorf("延时任务开始执行#参数不足#%+v", data)
return
}
id := data[0].(int64)
url := data[1].(string)
params := data[2].(string)
if id <= 0 || url == "" {
logger.Errorf("延时任务开始执行#参数为空#%+v", data)
return
}
taskModel := new(models.DelayTask)
_, err := taskModel.UpdateStatus(id, models.Running)
if err != nil {
+45 -38
View File
@@ -41,7 +41,7 @@
<div class="field">
<select name="status">
<option value="0">状态</option>
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} ></option>
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} ></option>
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>激活</option>
</select>
</div>
@@ -50,43 +50,50 @@
</div>
</div>
</form>
<div class="task-list">
{{{range $i, $v := .Tasks}}}
<div class="ui device two column middle aligned vertical grid segment">
<div class="column verborder">
<div class="ui info segment">
<h5 class="ui header">{{{.Task.Name}}} {{{if eq .Status 1}}}<i class="large checkmark blue icon"></i> {{{else}}} <i class="large red minus icon"></i> {{{end}}}
</h5>
<p>任务ID <span class="stress">{{{.Id}}}</span></p>
<p>状态 <span class="stress">{{{if eq .Status 1}}}激活{{{else}}}停止{{{end}}}</span></p>
<p>cron表达式 {{{.Spec}}}</p>
<p>执行方式: {{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SSH {{{else if eq .Protocol 3}}}本地命令{{{end}}}</p>
<p class="sensorStatus">命令{{{.Command}}}</p>
<p class="sensorStatus">超时时间{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</p>
<p>重试次数: {{{.RetryTimes}}}</p>
<p class="sensorStatus">是否允许多实例运行{{{if gt .Multi 0}}}{{{else}}}{{{end}}}</p>
{{{if eq .Protocol 2}}}
<p>主机: {{{.Alias}}}-{{{.Name}}}</p>
{{{end}}}
<p>备注: {{{.Remark}}}</p>
</div>
</div>
<div class="center aligned column">
<div class="ui buttons operation">
<a class="ui purple button" href="/task/edit/{{{.Id}}}">编辑</a>
{{{if eq .Status 1}}}
<button class="ui primary button" @click="changeStatus({{{.Id}}},{{{.Status}}})">停止</button>
{{{else}}}
<button class="ui blue button" @click="changeStatus({{{.Id}}},{{{.Status}}})">激活 </button>
{{{end}}}
<button class="ui positive button" @click="remove({{{.Id}}})">删除</button> <br>
<button class="ui twitter button" @click="run({{{.Id}}})">手动运行</button>
<a class="ui instagram button" href="/task/log?task_id={{{.Id}}}">查看日志</a>
</div>
</div>
</div>
{{{end}}}
</div>
<table class="ui pink table task-list">
<thead>
<tr>
<th>任务ID</th>
<th>任务名称</th>
<th>cron表达式</th>
<th>执行方式</th>
<th>超时时间</th>
<th>重试次数</th>
<th>单实例运行</th>
<th>主机</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{{{range $i, $v := .Tasks}}}
<tr>
<td>{{{.Id}}}</td>
<td>{{{.Task.Name}}}</td>
<td>{{{.Spec}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SSH {{{else if eq .Protocol 3}}}本地命令{{{end}}}</td>
<td>{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</td>
<td>{{{.RetryTimes}}}</td>
<td>{{{if gt .Multi 0}}}{{{else}}}{{{end}}}</td>
<td>{{{.Alias}}}-{{{.Name}}}</td>
<td>{{{if eq .Status 1}}}<span style="color: green;">激活</span>{{{else}}}<span style="color: red;">停止<span>{{{end}}}</td>
<td>
<div class="ui buttons operation">
<a class="ui purple button" href="/task/edit/{{{.Id}}}">编辑</a>
{{{if eq .Status 1}}}
<button class="ui primary button" @click="changeStatus({{{.Id}}},{{{.Status}}})">停止</button>
{{{else}}}
<button class="ui blue button" @click="changeStatus({{{.Id}}},{{{.Status}}})">激活 </button>
{{{end}}}
<button class="ui positive button" @click="remove({{{.Id}}})">删除</button> <br>
<button class="ui twitter button" @click="run({{{.Id}}})">手动运行</button>
<a class="ui instagram button" href="/task/log?task_id={{{.Id}}}">查看日志</a>
</div>
</td>
</tr>
{{{end}}}
</tbody>
</table>
{{{ template "common/pagination" .}}}
</div>
</div>
+2 -2
View File
@@ -11,7 +11,7 @@ fi
for i in linux darwin windows
do
./build.sh -p $i
if [[ ! $? ]];then
if [[ $? != 0 ]];then
break
fi
done
@@ -25,7 +25,7 @@ do
# 上传文件 qrsctl put bucket key srcFile
KEY=gocron/$i
qrsctl put github $KEY $i
if [[ ! $? ]];then
if [[ $? != 0 ]];then
break
fi
echo "刷新七牛CDN-" $QINIU_URL/$KEY
+3 -1
View File
@@ -252,7 +252,9 @@ func (engine *Engine) Close() error {
func (engine *Engine) Ping() error {
session := engine.NewSession()
defer session.Close()
engine.logger.Infof("PING DATABASE %v", engine.DriverName())
if engine.showSQL {
engine.logger.Infof("PING DATABASE %v", engine.DriverName())
}
return session.Ping()
}
+11 -9
View File
@@ -7,12 +7,15 @@ import (
// @author qiang.ou<qingqianludao@gmail.com>
type Job func([]interface{})
type TimeWheel struct {
interval time.Duration
ticker *time.Ticker
slots []*list.List
currentPos int
slotNum int
job Job
taskChannel chan Task
stopChannel chan bool
}
@@ -21,19 +24,18 @@ type TimeWheel struct {
type Task struct {
delay time.Duration
circle int
job Job
data []interface{}
}
type Job func()
func New(interval time.Duration, slotNum int) *TimeWheel {
if interval <= 0 || slotNum <= 0 {
func New(interval time.Duration, slotNum int, job Job) *TimeWheel {
if interval <= 0 || slotNum <= 0 || job == nil {
return nil
}
tw := &TimeWheel{
interval: interval,
slots: make([]*list.List, slotNum),
currentPos: 0,
job: job,
slotNum: slotNum,
taskChannel: make(chan Task),
stopChannel: make(chan bool),
@@ -55,11 +57,11 @@ func (tw *TimeWheel) Start() {
go tw.start()
}
func (tw *TimeWheel) Add(delay time.Duration, job Job) {
if delay < 0 || job == nil {
func (tw *TimeWheel) Add(delay time.Duration, data []interface{}) {
if delay <= 0 {
return
}
tw.taskChannel <- Task{delay:delay, job: job}
tw.taskChannel <- Task{delay:delay, data: data}
}
func (tw *TimeWheel) Stop() {
@@ -99,7 +101,7 @@ func (tw *TimeWheel) scanAndRunTask(l *list.List) {
continue
}
go task.job()
go tw.job(task.data)
next := e.Next()
l.Remove(e)
e = next