mtx-watch.lua /size: 16 Kb    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['mtx-watch'] = {
2    version   = 1.001,
3    comment   = "companion to mtxrun.lua",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9local helpinfo = [[
10<?xml version="1.0"?>
11<application>
12 <metadata>
13  <entry name="name">mtx-watch</entry>
14  <entry name="detail">ConTeXt Request Watchdog</entry>
15  <entry name="version">1.00</entry>
16 </metadata>
17 <flags>
18  <category name="basic">
19   <subcategory>
20    <flag name="logpath"><short>optional path for log files</short></flag>
21    <flag name="watch"><short>watch given path [<ref name="delay]"/></short></flag>
22    <flag name="pipe"><short>use pipe instead of execute</short></flag>
23    <flag name="delay"><short>delay between sweeps</short></flag>
24    <flag name="automachine"><short>replace /machine/ in path /servername/</short></flag>
25    <flag name="collect"><short>condense log files</short></flag>
26    <flag name="cleanup" value="delay"><short>remove files in given path [<ref name="force]"/></short></flag>
27    <flag name="showlog"><short>show log data</short></flag>
28   </subcategory>
29  </category>
30 </flags>
31</application>
32]]
33
34local application = logs.application {
35    name     = "mtx-watch",
36    banner   = "ConTeXt Request Watchdog 1.00",
37    helpinfo = helpinfo,
38}
39
40local report = application.report
41
42scripts       = scripts       or { }
43scripts.watch = scripts.watch or { }
44
45local format, concat, difftime, time = string.format, table.concat, os.difftime, os.time
46local next, type = next, type
47local basename, dirname, joinname = file.basename, file.dirname, file.join
48local lfsdir, lfsattributes = lfs.dir, lfs.attributes
49
50-- the machine/instance matches the server app we use
51
52local machine  = socket.dns.gethostname() or "unknown-machine"
53local instance = string.match(machine,"(%d+)$") or "0"
54
55function scripts.watch.save_exa_modes(joblog,ctmname)
56    local values = joblog and joblog.values
57    if values then
58        local t= { }
59        t[#t+1] = "<?xml version='1.0' standalone='yes'?>\n"
60        t[#t+1] = "<exa:variables xmlns:exa='htpp://www.pragma-ade.com/schemas/exa-variables.rng'>"
61        for k, v in next, joblog.values do
62            t[#t+1] = format("\t<exa:variable label='%s'>%s</exa:variable>", k, tostring(v))
63        end
64        t[#t+1] = "</exa:variables>"
65        io.savedata(ctmname,concat(t,"\n"))
66    else
67        os.remove(ctmname)
68    end
69end
70
71local function toset(t)
72    if type(t) == "table" then
73        return concat(t,",")
74    else
75        return t
76    end
77end
78
79local function noset(t)
80    if type(t) == "table" then
81        return t[1]
82    else
83        return t
84    end
85end
86
87-- todo: split order (o-name.luj) and combine with atime to determine sort order.
88
89local function glob(files,path) -- some day: sort by name (order prefix) and atime
90    for name in lfsdir(path) do
91        if name:find("^%.") then
92            -- skip . and ..
93        else
94            name = path .. "/" .. name
95            local a = lfsattributes(name)
96            if not a then
97                -- weird
98            elseif a.mode == "directory" then
99                if name:find("graphics$") or name:find("figures$") or name:find("resources$") then
100                    -- skip these too
101                else
102                    glob(files,name)
103                end
104            elseif name:find(".%luj$") then
105                local bname = basename(name)
106                local dname = dirname(name)
107                local order = tonumber(bname:match("^(%d+)")) or 0
108                files[#files+1] = { dname, bname, order }
109            end
110        end
111    end
112end
113
114local clock = os.gettimeofday or (socket and socket.gettime) or os.time -- we cannot trust os.clock on linux
115
116-- local function filenamesort(a,b)
117--     local fa, da = a[1], a[2]
118--     local fb, db = b[1], b[2]
119--     if da == db then
120--         return fa < fb
121--     else
122--         return da < db
123--     end
124-- end
125
126local function filenamesort(a,b)
127    local fa, oa = a[2], a[3]
128    local fb, ob = b[2], b[3]
129    if fa == fb then
130        if oa == ob then
131            return a[1] < b[1] -- order file  dir
132        else
133            return oa < ob     -- order file
134        end
135    else
136        if oa == ob then
137            return fa < fb     -- order file
138        else
139            return oa < ob     -- order file
140        end
141    end
142end
143
144function scripts.watch.watch()
145    local delay   = tonumber(environment.argument("delay") or 5) or 5
146    if delay == 0 then
147        delay = .25
148    end
149    local logpath = environment.argument("logpath") or ""
150    local pipe    = environment.argument("pipe")    or false
151    local watcher = "mtxwatch.run"
152    local paths   = environment.files
153    if #paths > 0 then
154        if environment.argument("automachine") then
155            logpath = string.gsub(logpath,"/machine/","/"..machine.."/")
156            for i=1,#paths do
157                paths[i] = string.gsub(paths[i],"/machine/","/"..machine.."/")
158            end
159        end
160        for i=1,#paths do
161            report("watching path %s",paths[i])
162        end
163        local function process()
164            local done = false
165            for i=1,#paths do
166                local path = paths[i]
167                lfs.chdir(path)
168                local files = { }
169                glob(files,path)
170                glob(files,".")
171                table.sort(files,filenamesort)
172--                 for name, time in next, files do
173                for i=1,#files do
174                    local f = files[i]
175                    local dirname = f[1]
176                    local basename = f[2] -- we can use that later on
177                    local name = joinname(dirname,basename)
178                --~ local ok, joblog = xpcall(function() return dofile(name) end, function() end )
179                    local ok, joblog = pcall(dofile,name)
180report("checking file %s/%s: %s",dirname,basename,ok and "okay" or "skipped")
181                    if ok and joblog then
182                        if joblog.status == "processing" then
183                            report("aborted job, %s added to queue",name)
184                            joblog.status = "queued"
185                            io.savedata(name, table.serialize(joblog,true))
186                        elseif joblog.status == "queued" then
187                            local command = joblog.command
188                            if command then
189                                local replacements = {
190                                    inputpath  = toset((joblog.paths and joblog.paths.input ) or "."),
191                                    outputpath = noset((joblog.paths and joblog.paths.output) or "."),
192                                    filename   = joblog.filename or "",
193                                }
194                                -- todo: revision path etc
195                                command = command:gsub("%%(.-)%%", replacements)
196                                if command ~= "" then
197                                    joblog.status = "processing"
198                                    joblog.runtime = clock()
199                                    io.savedata(name, table.serialize(joblog,true))
200                                    report("running: %s", command)
201                                    local newpath = file.dirname(name)
202                                    io.flush()
203                                    local result = ""
204                                    local ctmname = file.basename(replacements.filename)
205                                    if ctmname == "" then ctmname = name end -- use self as fallback
206                                    ctmname = file.replacesuffix(ctmname,"ctm")
207                                    if newpath ~= "" and newpath ~= "." then
208                                        local oldpath = lfs.currentdir()
209                                        lfs.chdir(newpath)
210                                        scripts.watch.save_exa_modes(joblog,ctmname)
211                                        if pipe then result = os.resultof(command) else result = os.execute(command) end
212                                        lfs.chdir(oldpath)
213                                    else
214                                        scripts.watch.save_exa_modes(joblog,ctmname)
215                                        if pipe then result = os.resultof(command) else result = os.execute(command) end
216                                    end
217                                    report("return value: %s", result)
218                                    done = true
219                                    local path, base = replacements.outputpath, file.basename(replacements.filename)
220                                    joblog.runtime = clock() - joblog.runtime
221                                    if base ~= "" then
222                                        joblog.result = file.replacesuffix(file.join(path,base),"pdf")
223                                        joblog.size   = lfs.attributes(joblog.result,"size")
224                                    end
225                                    joblog.status = "finished"
226                                else
227                                    joblog.status = "invalid command"
228                                end
229                            else
230                                joblog.status = "no command"
231                            end
232                            -- pcall, when error sleep + again
233                            -- todo: just one log file and append
234                            io.savedata(name, table.serialize(joblog,true))
235                            if logpath and logpath ~= "" then
236                                local name = file.join(logpath,os.uuid() .. ".lua")
237                                io.savedata(name, table.serialize(joblog,true))
238                                report("saving joblog in %s",name)
239                            end
240                        end
241                    end
242                end
243            end
244        end
245        local n, start = 0, time()
246        local wtime = 0
247        local function wait()
248            io.flush()
249            if not done then
250                n = n + 1
251                if n >= 10 then
252                    report("run time: %i seconds, memory usage: %0.3g MB", difftime(time(),start), (status.luastate_bytes/1024)/1000)
253                    n = 0
254                end
255                local ttime = 0
256                while ttime <= delay do
257                    local wt = lfs.attributes(watcher,"mtime")
258                    if wt and wt ~= wtime then
259                        -- fast signal that there is a request
260                        wtime = wt
261                        break
262                    end
263                    ttime = ttime + 0.2
264                    os.sleep(0.2)
265                end
266            end
267        end
268        local cleanupdelay, cleanup = environment.argument("cleanup"), false
269        if cleanupdelay then
270            local lasttime = time()
271            cleanup = function()
272                local currenttime = time()
273                local delta = difftime(currenttime,lasttime)
274                if delta > cleanupdelay then
275                    lasttime = currenttime
276                    for i=1,#paths do
277                        local path = paths[i]
278                        if string.find(path,"%.") then
279                            -- safeguard, we want a fully qualified path
280                        else
281                            local files = dir.glob(file.join(path,"*"))
282                            for i=1,#files do
283                                local name = files[i]
284                                local filetime = lfs.attributes(name,"modification")
285                                local delta = difftime(currenttime,filetime)
286                                if delta > cleanupdelay then
287                                 -- report("cleaning up '%s'",name)
288                                    os.remove(name)
289                                end
290                            end
291                        end
292                    end
293                end
294            end
295        else
296            cleanup = function()
297                -- nothing
298            end
299        end
300        while true do
301            if false then
302--~             if true then
303                process()
304                cleanup()
305                wait()
306            else
307                pcall(process)
308                pcall(cleanup)
309                pcall(wait)
310            end
311        end
312    else
313        report("no paths to watch")
314    end
315end
316
317function scripts.watch.collect_logs(path) -- clean 'm up too
318    path = path or environment.argument("logpath") or ""
319    path = (path == "" and ".") or path
320    local files = dir.globfiles(path,false,"^%d+%.lua$")
321    local collection = { }
322    local valid = table.tohash({"filename","result","runtime","size","status"})
323    for i=1,#files do
324        local name = files[i]
325        local t = dofile(name)
326        if t and type(t) == "table" and t.status then
327            for k, v in next, t do
328                if not valid[k] then
329                    t[k] = nil
330                end
331            end
332            collection[name:gsub("[^%d]","")] = t
333        end
334    end
335    return collection
336end
337
338function scripts.watch.save_logs(collection,path) -- play safe
339    if collection and next(collection) then
340        path = path or environment.argument("logpath") or ""
341        path = (path == "" and ".") or path
342        local filename = format("%s/collected-%s.lua",path,tostring(time()))
343        io.savedata(filename,table.serialize(collection,true))
344        local check = dofile(filename)
345        for k,v in next, check do
346            if not collection[k] then
347                report("error in saving file")
348                os.remove(filename)
349                return false
350            end
351        end
352        for k,v in next, check do
353            os.remove(format("%s.lua",k))
354        end
355        return true
356    else
357        return false
358    end
359end
360
361function scripts.watch.collect_collections(path) -- removes duplicates
362    path = path or environment.argument("logpath") or ""
363    path = (path == "" and ".") or path
364    local files = dir.globfiles(path,false,"^collected%-%d+%.lua$")
365    local collection = { }
366    for i=1,#files do
367        local name = files[i]
368        local t = dofile(name)
369        if t and type(t) == "table" then
370            for k, v in next, t do
371                collection[k] = v
372            end
373        end
374    end
375    return collection
376end
377
378function scripts.watch.show_logs(path) -- removes duplicates
379    local collection = scripts.watch.collect_collections(path) or { }
380    local max = 0
381    for k,v in next, collection do
382        v = v.filename or "?"
383        if #v > max then max = #v end
384    end
385 -- print(max)
386    local sorted = table.sortedkeys(collection)
387    for k=1,#sorted do
388        local v = sorted[k]
389        local c = collection[v]
390        local f, s, r, n = c.filename or "?", c.status or "?", c.runtime or 0, c.size or 0
391        report("%s  %s  %3i  %8i  %s",string.padd(f,max," "),string.padd(s,10," "),r,n,v)
392    end
393end
394
395function scripts.watch.cleanup_stale_files() -- removes duplicates
396    local path  = environment.files[1]
397    local delay = tonumber(environment.argument("cleanup"))
398    local force = environment.argument("force")
399    if not path or path == "." then
400        report("provide qualified path")
401    elseif not delay then
402        report("missing --cleanup=delay")
403    else
404        if not force then
405            report("dryrun, use --force for real cleanup")
406        end
407        local files = dir.glob(file.join(path,"*"))
408        local rtime = time()
409        for i=1,#files do
410            local name = files[i]
411            local mtime = lfs.attributes(name,"modification")
412            local delta = difftime(rtime,mtime)
413            if delta > delay then
414                report("cleaning up '%s'",name)
415                if force then
416                    os.remove(name)
417                end
418            end
419        end
420    end
421end
422
423if environment.argument("watch") then
424    scripts.watch.watch()
425elseif environment.argument("collect") then
426    scripts.watch.save_logs(scripts.watch.collect_logs())
427elseif environment.argument("cleanup") then
428    scripts.watch.save_logs(scripts.watch.cleanup_stale_files())
429elseif environment.argument("showlog") then
430    scripts.watch.show_logs()
431elseif environment.argument("exporthelp") then
432    application.export(environment.argument("exporthelp"),environment.files[1])
433else
434    application.help()
435end
436