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
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
88
89local function glob(files,path)
90 for name in lfsdir(path) do
91 if name:find("^%.") then
92
93 else
94 name = path .. "/" .. name
95 local a = lfsattributes(name)
96 if not a then
97
98 elseif a.mode == "directory" then
99 if name:find("graphics$") or name:find("figures$") or name:find("resources$") then
100
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
115
116
117
118
119
120
121
122
123
124
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]
132 else
133 return oa < ob
134 end
135 else
136 if oa == ob then
137 return fa < fb
138 else
139 return oa < ob
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
173 for i=1,#files do
174 local f = files[i]
175 local dirname = f[1]
176 local basename = f[2]
177 local name = joinname(dirname,basename)
178
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
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
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
233
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
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
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
288 os.remove(name)
289 end
290 end
291 end
292 end
293 end
294 end
295 else
296 cleanup = function()
297
298 end
299 end
300 while true do
301 if false then
302
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)
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)
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)
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)
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
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()
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 |