if not modules then modules = { } end modules ['mtx-watch'] = {
version = 1.001,
comment = "companion to mtxrun.lua",
author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
copyright = "PRAGMA ADE / ConTeXt Development Team",
license = "see context related readme files"
}
local helpinfo = [[
mtx-watch
ConTeXt Request Watchdog
1.00
optional path for log files
watch given path [
use pipe instead of execute
delay between sweeps
replace /machine/ in path /servername/
condense log files
remove files in given path [
show log data
]]
local application = logs.application {
name = "mtx-watch",
banner = "ConTeXt Request Watchdog 1.00",
helpinfo = helpinfo,
}
local report = application.report
scripts = scripts or { }
scripts.watch = scripts.watch or { }
local format, concat, difftime, time = string.format, table.concat, os.difftime, os.time
local next, type = next, type
local basename, dirname, joinname = file.basename, file.dirname, file.join
local lfsdir, lfsattributes = lfs.dir, lfs.attributes
-- the machine/instance matches the server app we use
local machine = socket.dns.gethostname() or "unknown-machine"
local instance = string.match(machine,"(%d+)$") or "0"
function scripts.watch.save_exa_modes(joblog,ctmname)
local values = joblog and joblog.values
if values then
local t= { }
t[#t+1] = "\n"
t[#t+1] = ""
for k, v in next, joblog.values do
t[#t+1] = format("\t%s", k, tostring(v))
end
t[#t+1] = ""
io.savedata(ctmname,concat(t,"\n"))
else
os.remove(ctmname)
end
end
local function toset(t)
if type(t) == "table" then
return concat(t,",")
else
return t
end
end
local function noset(t)
if type(t) == "table" then
return t[1]
else
return t
end
end
-- todo: split order (o-name.luj) and combine with atime to determine sort order.
local function glob(files,path) -- some day: sort by name (order prefix) and atime
for name in lfsdir(path) do
if name:find("^%.") then
-- skip . and ..
else
name = path .. "/" .. name
local a = lfsattributes(name)
if not a then
-- weird
elseif a.mode == "directory" then
if name:find("graphics$") or name:find("figures$") or name:find("resources$") then
-- skip these too
else
glob(files,name)
end
elseif name:find(".%luj$") then
local bname = basename(name)
local dname = dirname(name)
local order = tonumber(bname:match("^(%d+)")) or 0
files[#files+1] = { dname, bname, order }
end
end
end
end
local clock = os.gettimeofday or (socket and socket.gettime) or os.time -- we cannot trust os.clock on linux
-- local function filenamesort(a,b)
-- local fa, da = a[1], a[2]
-- local fb, db = b[1], b[2]
-- if da == db then
-- return fa < fb
-- else
-- return da < db
-- end
-- end
local function filenamesort(a,b)
local fa, oa = a[2], a[3]
local fb, ob = b[2], b[3]
if fa == fb then
if oa == ob then
return a[1] < b[1] -- order file dir
else
return oa < ob -- order file
end
else
if oa == ob then
return fa < fb -- order file
else
return oa < ob -- order file
end
end
end
function scripts.watch.watch()
local delay = tonumber(environment.argument("delay") or 5) or 5
if delay == 0 then
delay = .25
end
local logpath = environment.argument("logpath") or ""
local pipe = environment.argument("pipe") or false
local watcher = "mtxwatch.run"
local paths = environment.files
if #paths > 0 then
if environment.argument("automachine") then
logpath = string.gsub(logpath,"/machine/","/"..machine.."/")
for i=1,#paths do
paths[i] = string.gsub(paths[i],"/machine/","/"..machine.."/")
end
end
for i=1,#paths do
report("watching path %s",paths[i])
end
local function process()
local done = false
for i=1,#paths do
local path = paths[i]
lfs.chdir(path)
local files = { }
glob(files,path)
table.sort(files,filenamesort)
-- for name, time in next, files do
for i=1,#files do
local f = files[i]
local dirname = f[1]
local basename = f[2] -- we can use that later on
local name = joinname(dirname,basename)
--~ local ok, joblog = xpcall(function() return dofile(name) end, function() end )
local ok, joblog = pcall(dofile,name)
if ok and joblog then
if joblog.status == "processing" then
report("aborted job, %s added to queue",name)
joblog.status = "queued"
io.savedata(name, table.serialize(joblog,true))
elseif joblog.status == "queued" then
local command = joblog.command
if command then
local replacements = {
inputpath = toset((joblog.paths and joblog.paths.input ) or "."),
outputpath = noset((joblog.paths and joblog.paths.output) or "."),
filename = joblog.filename or "",
}
-- todo: revision path etc
command = command:gsub("%%(.-)%%", replacements)
if command ~= "" then
joblog.status = "processing"
joblog.runtime = clock()
io.savedata(name, table.serialize(joblog,true))
report("running: %s", command)
local newpath = file.dirname(name)
io.flush()
local result = ""
local ctmname = file.basename(replacements.filename)
if ctmname == "" then ctmname = name end -- use self as fallback
ctmname = file.replacesuffix(ctmname,"ctm")
if newpath ~= "" and newpath ~= "." then
local oldpath = lfs.currentdir()
lfs.chdir(newpath)
scripts.watch.save_exa_modes(joblog,ctmname)
if pipe then result = os.resultof(command) else result = os.spawn(command) end
lfs.chdir(oldpath)
else
scripts.watch.save_exa_modes(joblog,ctmname)
if pipe then result = os.resultof(command) else result = os.spawn(command) end
end
report("return value: %s", result)
done = true
local path, base = replacements.outputpath, file.basename(replacements.filename)
joblog.runtime = clock() - joblog.runtime
if base ~= "" then
joblog.result = file.replacesuffix(file.join(path,base),"pdf")
joblog.size = lfs.attributes(joblog.result,"size")
end
joblog.status = "finished"
else
joblog.status = "invalid command"
end
else
joblog.status = "no command"
end
-- pcall, when error sleep + again
-- todo: just one log file and append
io.savedata(name, table.serialize(joblog,true))
if logpath and logpath ~= "" then
local name = file.join(logpath,os.uuid() .. ".lua")
io.savedata(name, table.serialize(joblog,true))
report("saving joblog in %s",name)
end
end
end
end
end
end
local n, start = 0, time()
local wtime = 0
local function wait()
io.flush()
if not done then
n = n + 1
if n >= 10 then
report("run time: %i seconds, memory usage: %0.3g MB", difftime(time(),start), (status.luastate_bytes/1024)/1000)
n = 0
end
local ttime = 0
while ttime <= delay do
local wt = lfs.attributes(watcher,"mtime")
if wt and wt ~= wtime then
-- fast signal that there is a request
wtime = wt
break
end
ttime = ttime + 0.2
os.sleep(0.2)
end
end
end
local cleanupdelay, cleanup = environment.argument("cleanup"), false
if cleanupdelay then
local lasttime = time()
cleanup = function()
local currenttime = time()
local delta = difftime(currenttime,lasttime)
if delta > cleanupdelay then
lasttime = currenttime
for i=1,#paths do
local path = paths[i]
if string.find(path,"%.") then
-- safeguard, we want a fully qualified path
else
local files = dir.glob(file.join(path,"*"))
for i=1,#files do
local name = files[i]
local filetime = lfs.attributes(name,"modification")
local delta = difftime(currenttime,filetime)
if delta > cleanupdelay then
-- report("cleaning up '%s'",name)
os.remove(name)
end
end
end
end
end
end
else
cleanup = function()
-- nothing
end
end
while true do
if false then
--~ if true then
process()
cleanup()
wait()
else
pcall(process)
pcall(cleanup)
pcall(wait)
end
end
else
report("no paths to watch")
end
end
function scripts.watch.collect_logs(path) -- clean 'm up too
path = path or environment.argument("logpath") or ""
path = (path == "" and ".") or path
local files = dir.globfiles(path,false,"^%d+%.lua$")
local collection = { }
local valid = table.tohash({"filename","result","runtime","size","status"})
for i=1,#files do
local name = files[i]
local t = dofile(name)
if t and type(t) == "table" and t.status then
for k, v in next, t do
if not valid[k] then
t[k] = nil
end
end
collection[name:gsub("[^%d]","")] = t
end
end
return collection
end
function scripts.watch.save_logs(collection,path) -- play safe
if collection and next(collection) then
path = path or environment.argument("logpath") or ""
path = (path == "" and ".") or path
local filename = format("%s/collected-%s.lua",path,tostring(time()))
io.savedata(filename,table.serialize(collection,true))
local check = dofile(filename)
for k,v in next, check do
if not collection[k] then
report("error in saving file")
os.remove(filename)
return false
end
end
for k,v in next, check do
os.remove(format("%s.lua",k))
end
return true
else
return false
end
end
function scripts.watch.collect_collections(path) -- removes duplicates
path = path or environment.argument("logpath") or ""
path = (path == "" and ".") or path
local files = dir.globfiles(path,false,"^collected%-%d+%.lua$")
local collection = { }
for i=1,#files do
local name = files[i]
local t = dofile(name)
if t and type(t) == "table" then
for k, v in next, t do
collection[k] = v
end
end
end
return collection
end
function scripts.watch.show_logs(path) -- removes duplicates
local collection = scripts.watch.collect_collections(path) or { }
local max = 0
for k,v in next, collection do
v = v.filename or "?"
if #v > max then max = #v end
end
-- print(max)
local sorted = table.sortedkeys(collection)
for k=1,#sorted do
local v = sorted[k]
local c = collection[v]
local f, s, r, n = c.filename or "?", c.status or "?", c.runtime or 0, c.size or 0
report("%s %s %3i %8i %s",string.padd(f,max," "),string.padd(s,10," "),r,n,v)
end
end
function scripts.watch.cleanup_stale_files() -- removes duplicates
local path = environment.files[1]
local delay = tonumber(environment.argument("cleanup"))
local force = environment.argument("force")
if not path or path == "." then
report("provide qualified path")
elseif not delay then
report("missing --cleanup=delay")
else
report("dryrun, use --force for real cleanup")
local files = dir.glob(file.join(path,"*"))
local rtime = time()
for i=1,#files do
local name = files[i]
local mtime = lfs.attributes(name,"modification")
local delta = difftime(rtime,mtime)
if delta > delay then
report("cleaning up '%s'",name)
if force then
os.remove(name)
end
end
end
end
end
if environment.argument("watch") then
scripts.watch.watch()
elseif environment.argument("collect") then
scripts.watch.save_logs(scripts.watch.collect_logs())
elseif environment.argument("cleanup") then
scripts.watch.save_logs(scripts.watch.cleanup_stale_files())
elseif environment.argument("showlog") then
scripts.watch.show_logs()
elseif environment.argument("exporthelp") then
application.export(environment.argument("exporthelp"),environment.files[1])
else
application.help()
end