casyup.me@outlook.com

0%

other/crontab

需求描述

设计一个定时关机的工具

该工具使用lua语言, 通过PLINK链接服务器

接收用户输入, 使用crontab/at设置定时器

详细代码

timer table

1
2
3
4
5
6
7
8
9
10
11
12
13
package.path = package.path .. ";../../script/?.lua"
-- inc是工具类
local inc = require("inc")
-- config记录了服务器的配置信息
local config = require("config")

-- timer用于处理整个定时任务
local timer = {}
timer.file = "crontabtask.sh" -- 文件名, 因为使用 crontab [file]形式添加任务
timer.path = "/tmp/" -- 文件位于服务器那个路径下
timer.fullName = timer.path..timer.file -- 路径+文件名, 当前为: /tmp/crontabtask.sh
timer.filenamemask = "CRONTAB_TASK_" -- 掩码, 用于区分任务
timer.servername = nil -- 服务器名称, 这里仅仅占位, 会在之后设置

timer是任务处理表(类), 因为bash下, 没有办法直接使用vim编辑(或者说我不知道有什么办法能这么做)

所以 crontab -e 的方式被舍弃了, 使用 crontab [filename] 的形式来添加任务

(这样的好处是统一使用某一文件作为任务文件, 后续可以使用某种手段(比如修改配置), 来重新定义crontab -e)

(坏处就是动到了系统的东西, 这并不一定是好事)

lobby

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
-- 大厅
function timer.lobby()
inc.p("请输入要操作的服务器ID", 10)
local serverID = tonumber(io.stdin:read())
local scfg, servercfg = inc.getserverinfo(serverID)

inc.confirm_oper_server(scfg, {servercfg}, "即将操作该服务器")
-- 检查文件是否存在
timer.checkFileExists(scfg, servercfg)
local sFolder = string.format("%s%d.p%d",
config.gamename, servercfg.id, servercfg.port)
timer.servername = sFolder
-- 根据用户的选项, 我们重新设置了文件掩码, 加上了当前服务器名
-- 这样更加安全, 也可以筛选统一主机下, 不同服务器任务了
timer.filenamemask = timer.filenamemask..timer.servername
while (true)
do
inc.p("\n选择操作类型[1: 增加, 2: 删除, 4:查询]", 10)
local operatetype = io.stdin:read()
if operatetype == '1' then
-- 增加定时器
timer.addTimerTask(scfg, servercfg)
-- 判断是否增加成功
timer.isAddSuccess(scfg, servercfg)
-- 自动刷新定时器列表
timer.searchTimerTask(scfg, servercfg)
elseif operatetype == '2' then
-- 删除定时器
timer.deleteTimerTask(scfg, servercfg)
-- 自动刷新定时器列表
timer.searchTimerTask(scfg, servercfg)
elseif operatetype == '3' then
-- 预留
elseif operatetype == '4' then
-- 刷新定时器列表
timer.searchTimerTask(scfg, servercfg)
else
inc.p("操作码异常", 14)
end
end
end

大厅界面, 先让用户选择一个操作的服务器, 设置相应的参数, 然后让用户一直操作该服务器

checkFileExists

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- 检查文件是否存在
function timer.checkFileExists(scfg, servercfg)
-- 看一下目录下是否存配置文件
local cmds = {}
table.insert(cmds, "cd "..timer.path)
table.insert(cmds, "ls crontab*");
local filename;
inc.popen_server_cmds(scfg, cmds, function (res)
-- 因为某些原因, 不用在意这行代码. 它的作用是获取文件名
filename = res[2]
end, true)

if (string.find(filename, "crontab") == nil) then
-- 没找到配置文件, 就新建一个
inc.p("未找到文件, 即将创建空文件: ", 10)
local cmds = {}
table.insert(cmds, "touch "..timer.fullName)
inc.popen_server_cmds(scfg, cmds, nil, true)
inc.p("创建成功: ", 10)
else
-- 如果配置文件已经存在了, 那么就直接使用当前文件
inc.p("已找到文件: "..filename, 10)
timer.file = filename
timer.fullName = timer.path..timer.file
end
end

检查配置文件是否已存在, 存在则世界使用它, 不存在, 则创建一个

addTimerTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
-- 增加定时器
function timer.addTimerTask(scfg, servercfg)
-- 设置时间
local timetab = {}
inc.p("请输入任务名", 10)
local taskname = io.stdin:read()
local ttaskname = taskname..timer.filenamemask
-- 检测任务名是否存在, 任务名用于删, 查任务
while (timer.checkTaskName(scfg, servercfg, ttaskname) == true) do
inc.p("任务重名, 请重新输入", 14)
taskname = io.stdin:read()
ttaskname = taskname..timer.filenamemask
end

-- 设置定时器触发时间
inc.p("请输入服务器关闭时间(月)", 10)
timetab.month = io.stdin:read()
inc.p("请输入服务器关闭时间(日)", 10)
timetab.day = io.stdin:read()
inc.p("请输入服务器关闭时间(时)", 10)
timetab.hour = io.stdin:read()
inc.p("请输入服务器关闭时间(分)", 10)
timetab.minute = io.stdin:read()
inc.p("服务器将计划于 "..timetab.month.."月"..timetab.day.."日"..
timetab.hour.."时"..timetab.minute.."分 关闭, 确定? [输入y继续]", 12)
if io.stdin:read() ~= 'y' then return end

-- 这里构造了一条命令, 这条命令实现了添加一个定时器
-- 这个定时器会到指定的服务器下, 调用 kill.net 来关闭服务器
-- 关闭之后, 再使用 sed 命令, 将定时器删除, 然后重置定时器
local cmds = {};
local dir = timer.getFullPath(servercfg)
local timestamp = timetab.minute..' '..timetab.hour..' '..timetab.day..' '..
timetab.month..' '..'*'
table.insert(cmds, "echo \""..timestamp.." echo \""..ttaskname.."\""
..';cd '..dir..';./kill.net'..
";sed -i \"/"..ttaskname.."/d\" "..timer.fullName..
";crontab "..timer.fullName.."\" >> "..timer.fullName)
print (cmds[1]);
-- 这是一个外部工具类中的函数, 主要是使用PLINK携带用户信息和验证, 链接服务器
-- 然后执行 cmds 中保存的命令
inc.popen_server_cmds(scfg, cmds, nil, true)
-- 保存当前任务名, 用于判断任务是否成功
timer.lastTask = ttaskname;
timer.syncTask(scfg, servercfg)
end

checkTaskName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 检查文件名
function timer.checkTaskName(scfg, servercfg, ttaskname)
local cmds = {}
table.insert(cmds, "crontab -l")
local dupname = false
-- 第三个参数是回调函数, res中存储了执行 cmds 后, 服务器的输出
-- 使用 crontab -l 查看了当前已有的任务, 如果找到了任务名, 则说明任务名重复
inc.popen_server_cmds(scfg, cmds, function (res)
for i, v in pairs(res) do
if (string.find(v, ttaskname)) then
dupname = true
return
end
end
end, true)

return dupname
end

syncTask

1
2
3
4
5
6
7
8
-- 同步任务
function timer.syncTask(scfg, servercfg)
local cmds = {}
table.insert(cmds, "cd "..timer.path)
table.insert(cmds, "crontab "..'./'..timer.file);
-- 其实就是执行 crontab [filename], 这里可以合为一句的
inc.popen_server_cmds(scfg, cmds, nil, true)
end

isAddSuccess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- 是否增加成功
function timer.isAddSuccess(scfg, servercfg)
local cmds = {}
table.insert(cmds, "cat "..timer.fullName);
local size1 = 0;
-- 获取文件中的行数
inc.popen_server_cmds(scfg, cmds, function (res)
size1 = #res
end, true)

local cmds = {}
table.insert(cmds, "crontab -l");
local size2 = 0;
-- 获取实际 crontab 任务的行数
inc.popen_server_cmds(scfg, cmds, function (res)
size2 = #res
end, true)

-- 如果行数不一致, 那么就说明加入失败
-- 加入失败一般只有一种原因: 时间格式错误
-- 我们也可以编写代码在执行加入前就判断, 但是判断时间的话, 涉及到平年和润年, 还涉及大小月
-- 并且是服务器的时间, 所以也不能直接在windows上调用函数处理
-- 考虑到这些原因, 就让linux帮我们做了这件事(反正行数不一致, 肯定是失败了)
if size1 ~= size2 then
-- 添加失败就回滚一次任务
timer.rollBack(scfg, servercfg)
else
inc.p("添加定时任务成功", 14)
end
end

rollBack

1
2
3
4
5
6
7
8
9
-- 回滚一次任务
function timer.rollBack(scfg, servercfg)
local cmds = {}
-- 这里使用了 sed 来处理, 其中用到了我们之前记录的 lastTask
table.insert(cmds, "sed -i "..'/'..timer.lastTask.."/d"..
' '..timer.fullName);
inc.popen_server_cmds(scfg, cmds, nil, true)
inc.p("时间格式错误, 已回滚", 12)
end

searchTimerTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 查询定时器
function timer.searchTimerTask(scfg, servercfg)
local cmds = {}
table.insert(cmds, "crontab -l")
inc.p("\n当前已有任务: ", 10)
local orisrc = {}
-- 查看当前已有的任务
inc.popen_server_cmds(scfg, cmds, function (res)
orisrc = res
end, true)

-- 这里是为了将数据格式化成方便看懂的格式
timer.regex(orisrc, servercfg)
end

regex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- 检测记录, 筛选信息
function timer.regex(orisrc, servercfg)
for i, v in pairs(orisrc) do
-- 获取任务时间戳
local taskstamp;
for v2 in string.gmatch(v, "%d+%s%d+%s%d+%s%d+") do
taskstamp = v2
end

-- 获取任务名
local taskname;
for v3 in string.gmatch(v, "echo.*"..timer.filenamemask..';') do
v3 = string.sub(v3, 6, #v3 - #timer.filenamemask - 1)
taskname = v3
end

-- 获取服务器名
local hostname;
for v4 in string.gmatch(v, "CRONTAB_TASK.*;cd%s/data/server/") do
v4 = string.sub(v4, 14, #v4 - 17)
hostname = v4
end

-- 如果这条信息是任务信息, 格式化打印出来
if (taskstamp ~= nil and taskname ~= nil and hostname == timer.servername) then
local timetab = {}
for i in string.gmatch(taskstamp, "%d+") do
table.insert(timetab, i);
end

inc.p("服务器: "..hostname.." ======== 任务名: "..taskname..
" ======== 时间: "..
timetab[4].."月"..timetab[3].."日"..
timetab[2].."时"..timetab[1].."分", 14)
end
end
end

deleteTimerTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 删除定时器
function timer.deleteTimerTask(scfg, servercfg)
inc.p("请输入任务名(暂不支持中文)", 10)
local taskname = io.stdin:read()
local dir = timer.getFullPath(servercfg)
local cmds = {}
table.insert(cmds, "cd "..dir)
local ttaskname = taskname..timer.filenamemask
-- 执行一条 sed 命令, 删除一个任务
table.insert(cmds, "sed -i "..'/'..ttaskname.."/d"..
' '..timer.fullName);
inc.popen_server_cmds(scfg, cmds, nil, true)
-- 同步一次
timer.syncTask(scfg, servercfg)
end

summary

那么代码就这些了, 其中主要就是使用lua编写程序, 通过PLINK传递命令行信息

以此达到远程控制服务器定时器的效果

其中最主要的技术是:

lua语言基础, lua正则表达式

{ crontab, sed } 命令行, 数据流重定向

(emmmmmm… 看起来好像没什么厉害的…)

以及从<重构>中学到的代码技术

(一开始看重构这本书的时候, 本来对里面一些降低效率的做法不太满意)

(但是真的使用了之后, 发现代码的确好多了, 无论是易读, 编写, 调试, 增加/删除方面, 都有明显提升)

(不过可惜没有运用到<设计模式>中的东西(或许我用到了, 只是没注意到?) )