trac-log.lua /size: 15 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['trac-log'] = {
2    version   = 1.001,
3    comment   = "companion to trac-log.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- In fact all writes could go through lua and we could write the console and
10-- terminal handler in lua then. Ok, maybe it's slower then, so a no-go.
11--
12-- This is the version for mtxrun. The alternative functions for the TeX engines
13-- have been separated. In order to keep in sync we use tex specific witer names.
14--
15-- Todo: some cleanup (less local needed).
16
17local next, type, select, print = next, type, select, print
18local format, gmatch, find = string.format, string.gmatch, string.find
19local concat, insert, remove = table.concat, table.insert, table.remove
20local topattern = string.topattern
21local utfchar = utf.char
22local datetime = os.date
23local sleep = os.sleep
24local openfile = io.open
25
26local write_nl = print
27local write    = io.write
28
29local setmetatableindex = table.setmetatableindex
30local formatters        = string.formatters
31local settings_to_hash  = utilities.parsers.settings_to_hash
32local sortedkeys        = table.sortedkeys
33
34local variant = "default"
35----- variant = "ansi"
36
37logs       = logs or { }
38local logs = logs
39
40local moreinfo = [[
41More information about ConTeXt and the tools that come with it can be found at:
42]] .. "\n" .. [[
43maillist : ntg-context@ntg.nl / http://www.ntg.nl/mailman/listinfo/ntg-context
44webpage  : http://www.pragma-ade.nl / http://tex.aanhet.net
45wiki     : http://contextgarden.net
46]]
47
48formatters.add (
49    formatters, "unichr",
50    [["U+" .. format("%%05X",%s) .. " (" .. utfchar(%s) .. ")"]]
51)
52
53formatters.add (
54    formatters, "chruni",
55    [[utfchar(%s) .. " (U+" .. format("%%05X",%s) .. ")"]]
56)
57
58-- basic loggers
59
60local function ignore() end
61
62setmetatableindex(logs, function(t,k) t[k] = ignore ; return ignore end)
63
64local report, subreport, status, settarget, setformats, settranslations
65
66local direct, subdirect, writer, pushtarget, poptarget, setlogfile, settimedlog, setprocessor, setformatters, newline
67
68-- we use formatters but best check for % then because for simple messages but
69-- we don't want this overhead for single messages (not that there are that
70-- many; we could have a special weak table)
71
72local function ansisupported(specification)
73    if specification ~= "ansi" and specification ~= "ansilog" then
74        return false
75    elseif os and os.enableansi then
76        return os.enableansi()
77    else
78        return false
79    end
80end
81
82do
83
84    local report_yes, subreport_yes, status_yes
85    local report_nop, subreport_nop, status_nop
86
87    local variants = {
88        default = {
89            formats = {
90                report_yes    = formatters["%-15s | %s"],
91                report_nop    = formatters["%-15s |"],
92                subreport_yes = formatters["%-15s | %s | %s"],
93                subreport_nop = formatters["%-15s | %s |"],
94                status_yes    = formatters["%-15s : %s\n"],
95                status_nop    = formatters["%-15s :\n"],
96            },
97        },
98        ansi = {
99            formats = {
100                report_yes    = formatters["%-15s | %s"],
101                report_nop    = formatters["%-15s |"],
102                subreport_yes = formatters["%-15s | %s | %s"],
103                subreport_nop = formatters["%-15s | %s |"],
104                status_yes    = formatters["%-15s : %s\n"],
105                status_nop    = formatters["%-15s :\n"],
106            },
107        },
108    }
109
110    logs.flush = ignore
111
112    writer = function(s)
113        write_nl(s)
114    end
115
116    newline = function()
117        write_nl("\n")
118    end
119
120    report = function(a,b,c,...)
121        if c then
122            write_nl(report_yes(a,formatters[b](c,...)))
123        elseif b then
124            write_nl(report_yes(a,b))
125        elseif a then
126            write_nl(report_nop(a))
127        else
128            write_nl("")
129        end
130    end
131
132    subreport = function(a,sub,b,c,...)
133        if c then
134            write_nl(subreport_yes(a,sub,formatters[b](c,...)))
135        elseif b then
136            write_nl(subreport_yes(a,sub,b))
137        elseif a then
138            write_nl(subreport_nop(a,sub))
139        else
140            write_nl("")
141        end
142    end
143
144    status = function(a,b,c,...) -- not to be used in lua anyway
145        if c then
146            write_nl(status_yes(a,formatters[b](c,...)))
147        elseif b then
148            write_nl(status_yes(a,b)) -- b can have %'s
149        elseif a then
150            write_nl(status_nop(a))
151        else
152            write_nl("\n")
153        end
154    end
155
156    direct          = ignore
157    subdirect       = ignore
158
159    settarget       = ignore
160    pushtarget      = ignore
161    poptarget       = ignore
162    setformats      = ignore
163    settranslations = ignore
164
165    setprocessor = function(f)
166        local writeline = write_nl
167        write_nl = function(s)
168            writeline(f(s))
169        end
170    end
171
172    setformatters = function(specification)
173        local f = nil
174        local d = variants.default
175        if specification then
176            if type(specification) == "table" then
177                f = specification.formats or specification
178            else
179                if not ansisupported(specification) then
180                    specification = "default"
181                end
182                local v = variants[specification]
183                if v then
184                    f = v.formats
185                end
186            end
187        end
188        if f then
189            d = d.formats
190        else
191            f = d.formats
192            d = f
193        end
194        setmetatableindex(f,d)
195        report_yes    = f.report_yes
196        report_nop    = f.report_nop
197        subreport_yes = f.subreport_yes
198        subreport_nop = f.subreport_nop
199        status_yes    = f.status_yes
200        status_nop    = f.status_nop
201    end
202
203    setformatters(variant)
204
205    setlogfile = function(name,keepopen)
206        if name and name ~= "" then
207            local localtime = os.localtime
208            local writeline = write_nl
209            if keepopen then
210                local f = io.open(name,"ab")
211                write_nl = function(s)
212                    writeline(s)
213                    f:write(localtime()," | ",s,"\n")
214                end
215            else
216                write_nl = function(s)
217                    writeline(s)
218                    local f = io.open(name,"ab")
219                    f:write(localtime()," | ",s,"\n")
220                    f:close()
221                end
222            end
223        end
224        setlogfile = ignore
225    end
226
227    settimedlog = function()
228        local localtime = os.localtime
229        local writeline = write_nl
230        write_nl = function(s)
231            writeline(localtime() .. " | " .. s)
232        end
233        settimedlog = ignore
234    end
235
236end
237
238logs.report          = report
239logs.subreport       = subreport
240logs.status          = status
241logs.settarget       = settarget
242logs.pushtarget      = pushtarget
243logs.poptarget       = poptarget
244logs.setformats      = setformats
245logs.settranslations = settranslations
246
247logs.setlogfile      = setlogfile
248logs.settimedlog     = settimedlog
249logs.setprocessor    = setprocessor
250logs.setformatters   = setformatters
251
252logs.direct          = direct
253logs.subdirect       = subdirect
254logs.writer          = writer
255logs.newline         = newline
256
257-- installer
258
259-- todo: renew (un) locks when a new one is added and wildcard
260
261local data   = { }
262local states = nil
263local force  = false
264
265function logs.reporter(category,subcategory)
266    local logger = data[category]
267    if not logger then
268        local state = states == true
269        if not state and type(states) == "table" then
270            for c, _ in next, states do
271                if find(category,c) then
272                    state = true
273                    break
274                end
275            end
276        end
277        logger = {
278            reporters = { },
279            state     = state,
280        }
281        data[category] = logger
282    end
283    local reporter = logger.reporters[subcategory or "default"]
284    if not reporter then
285        if subcategory then
286            reporter = function(...)
287                if force or not logger.state then
288                    subreport(category,subcategory,...)
289                end
290            end
291            logger.reporters[subcategory] = reporter
292        else
293            local tag = category
294            reporter = function(...)
295                if force or not logger.state then
296                    report(category,...)
297                end
298            end
299            logger.reporters.default = reporter
300        end
301    end
302    return reporter
303end
304
305logs.new = logs.reporter -- for old times sake
306
307-- context specicific: this ends up in the macro stream
308
309local ctxreport = logs.writer
310
311function logs.setmessenger(m)
312    ctxreport = m
313end
314
315function logs.messenger(category,subcategory)
316    -- we need to avoid catcode mess (todo: fast context)
317    if subcategory then
318        return function(...)
319            ctxreport(subdirect(category,subcategory,...))
320        end
321    else
322        return function(...)
323            ctxreport(direct(category,...))
324        end
325    end
326end
327
328-- so far
329
330local function setblocked(category,value) -- v.state == value == true : disable
331    if category == true or category == "all" then
332        -- lock all
333        category, value = "*", true
334    elseif category == false then
335        -- unlock all
336        category, value = "*", false
337    elseif value == nil then
338        -- lock selective
339        value = true
340    end
341    if category == "*" then
342        states = value
343        for k, v in next, data do
344            v.state = value
345        end
346    else
347        alllocked = false
348        states = settings_to_hash(category,type(states)=="table" and states or nil)
349        for c in next, states do
350            local v = data[c]
351            if v then
352                v.state = value
353            else
354                c = topattern(c,true,true)
355                for k, v in next, data do
356                    if find(k,c) then
357                        v.state = value
358                    end
359                end
360            end
361        end
362    end
363end
364
365function logs.disable(category,value)
366    setblocked(category,value == nil and true or value)
367end
368
369function logs.enable(category)
370    setblocked(category,false)
371end
372
373function logs.categories()
374    return sortedkeys(data)
375end
376
377function logs.show()
378    local n, c, s, max = 0, 0, 0, 0
379    for category, v in table.sortedpairs(data) do
380        n = n + 1
381        local state = v.state
382        local reporters = v.reporters
383        local nc = #category
384        if nc > c then
385            c = nc
386        end
387        for subcategory, _ in next, reporters do
388            local ns = #subcategory
389            if ns > c then
390                s = ns
391            end
392            local m = nc + ns
393            if m > max then
394                max = m
395            end
396        end
397        local subcategories = concat(sortedkeys(reporters),", ")
398        if state == true then
399            state = "disabled"
400        elseif state == false then
401            state = "enabled"
402        else
403            state = "unknown"
404        end
405        -- no new here
406        report("logging","category %a, subcategories %a, state %a",category,subcategories,state)
407    end
408    report("logging","categories: %s, max category: %s, max subcategory: %s, max combined: %s",n,c,s,max)
409end
410
411local delayed_reporters = { }
412
413setmetatableindex(delayed_reporters,function(t,k)
414    local v = logs.reporter(k.name)
415    t[k] = v
416    return v
417end)
418
419function utilities.setters.report(setter,...)
420    delayed_reporters[setter](...)
421end
422
423directives.register("logs.blocked", function(v)
424    setblocked(v,true)
425end)
426
427directives.register("logs.target", function(v)
428    settarget(v)
429end)
430
431-- we don't have show_open and show_close callbacks yet
432
433----- report_files = logs.reporter("files")
434local nesting      = 0
435local verbose      = false
436local hasscheme    = url.hasscheme
437
438-- there may be scripts out there using this:
439
440local simple = logs.reporter("comment")
441
442logs.simple     = simple
443logs.simpleline = simple
444
445-- obsolete
446
447logs.setprogram   = ignore -- obsolete
448logs.extendbanner = ignore -- obsolete
449logs.reportlines  = ignore -- obsolete
450logs.reportbanner = ignore -- obsolete
451logs.reportline   = ignore -- obsolete
452logs.simplelines  = ignore -- obsolete
453logs.help         = ignore -- obsolete
454
455-- applications
456
457local Carg, C, lpegmatch = lpeg.Carg, lpeg.C, lpeg.match
458local p_newline = lpeg.patterns.newline
459
460local linewise = (
461    Carg(1) * C((1-p_newline)^1) / function(t,s) t.report(s) end
462  + Carg(1) * p_newline^2        / function(t)   t.report()  end
463  + p_newline
464)^1
465
466local function reportlines(t,str)
467    if str then
468        lpegmatch(linewise,str,1,t)
469    end
470end
471
472local function reportbanner(t)
473    local banner = t.banner
474    if banner then
475        t.report(banner)
476        t.report()
477    end
478end
479
480local function reportversion(t)
481    local banner = t.banner
482    if banner then
483        t.report(banner)
484    end
485end
486
487local function reporthelp(t,...)
488    local helpinfo = t.helpinfo
489    if type(helpinfo) == "string" then
490        reportlines(t,helpinfo)
491    elseif type(helpinfo) == "table" then
492        for i=1,select("#",...) do
493            reportlines(t,t.helpinfo[select(i,...)])
494            if i < n then
495                t.report()
496            end
497        end
498    end
499end
500
501local function reportinfo(t)
502    t.report()
503    reportlines(t,t.moreinfo)
504end
505
506local function reportexport(t,method)
507    report(t.helpinfo)
508end
509
510local reporters = {
511    lines   = reportlines, -- not to be overloaded
512    banner  = reportbanner,
513    version = reportversion,
514    help    = reporthelp,
515    info    = reportinfo,
516    export  = reportexport,
517}
518
519local exporters = {
520    -- empty
521}
522
523logs.reporters = reporters
524logs.exporters = exporters
525
526function logs.application(t)
527    --
528    local arguments = environment and environment.arguments
529    if arguments then
530        local ansi = arguments.ansi or arguments.ansilog
531        if ansi then
532            logs.setformatters(arguments.ansi and "ansi" or "ansilog")
533        end
534    end
535    --
536    t.name     = t.name   or "unknown"
537    t.banner   = t.banner
538    t.moreinfo = moreinfo
539    t.report   = logs.reporter(t.name)
540    t.help     = function(...)
541        reporters.banner(t)
542        reporters.help(t,...)
543        reporters.info(t)
544    end
545    t.export   = function(...)
546        reporters.export(t,...)
547    end
548    t.identify = function()
549        reporters.banner(t)
550    end
551    t.version  = function()
552        reporters.version(t)
553    end
554    return t
555end
556
557-- somewhat special .. will be redone (already a better solution in place in lmx)
558
559-- logging to a file
560
561-- local syslogname = "oeps.xxx"
562--
563-- for i=1,10 do
564--     logs.system(syslogname,"context","test","fonts","font %s recached due to newer version (%s)","blabla","123")
565-- end
566
567local f_syslog = formatters["%s %s => %s => %s => %s\r"]
568
569function logs.system(whereto,process,jobname,category,fmt,arg,...)
570    local message = f_syslog(datetime("%d/%m/%y %H:%m:%S"),process,jobname,category,arg == nil and fmt or format(fmt,arg,...))
571    for i=1,10 do
572        local f = openfile(whereto,"a") -- we can consider keeping the file open
573        if f then
574            f:write(message)
575            f:close()
576            break
577        else
578            sleep(0.1)
579        end
580    end
581end
582
583local report_system = logs.reporter("system","logs")
584
585if utilities then
586    utilities.report = report_system
587end
588
589-- this is somewhat slower but prevents out-of-order messages when print is mixed
590-- with texio.write
591
592-- io.stdout:setvbuf('no')
593-- io.stderr:setvbuf('no')
594
595-- windows: > nul  2>&1
596-- unix   : > null 2>&1
597
598if package.helpers.report then
599    package.helpers.report = logs.reporter("package loader") -- when used outside mtxrun
600end
601