Hog v10.38.0
Logger.tcl
Go to the documentation of this file.
1 # Copyright 2018-2026 The University of Birmingham
2 # Copyright 2018-2026 Max-Planck-Institute for Physics
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 # @file Logger.tcl
17 # Logger functions for the Hog project
18 
19 
20 set DEBUG_MODE 0
21 
22 proc setDebugMode {mode} {
23  global DEBUG_MODE
24  set DEBUG_MODE $mode
25 }
26 
27 proc getDebugMode {} {
28  global DEBUG_MODE
29  return $DEBUG_MODE
30 }
31 
32 proc printDebugMode {} {
33  global DEBUG_MODE
34  if {$DEBUG_MODE} {
35  Msg Info "DEBUG_MODE is set to $DEBUG_MODE"
36  } else {
37  Msg Info "DEBUG_MODE is not set or is 0"
38  }
39 }
40 
41 ## @brief Safely get a value from a dictionary
42 #
43 # @param[in] d The dictionary to search
44 # @param[in] args The keys to look for
45 proc dictSafeGet {d args} {
46  if {[dict exists $d {*}$args]} {
47  return [dict get $d {*}$args]
48  } else {
49  return ""
50  }
51 }
52 
53 #### DESKTOP NOTIFICATIONS
54 
55 # Absolute path of the Hog images folder, resolved once when this file is
56 # sourced. The procedures below may be called from any working directory, so
57 # the path cannot be recomputed from [info script] at call time.
58 set HogImagesPath [file normalize [file join [file dirname [info script]] .. .. images]]
59 
60 # Reentrancy guard. Msg notifies on errors, so Notify must never be able to
61 # call back into Msg in a way that would trigger a second notification.
62 set NotifyInProgress 0
63 
64 ## @brief Check if desktop notifications have been enabled by the user
65 #
66 # Notifications are opt-in and controlled by the HOG_NOTIFY environment
67 # variable, which may be set to 1, true, yes, on or enabled.
68 #
69 # @returns 1 if notifications are enabled, 0 otherwise
70 #
71 proc NotifyEnabled {} {
72  if {![info exists ::env(HOG_NOTIFY)]} {
73  return 0
74  }
75  set value [string tolower [string trim $::env(HOG_NOTIFY)]]
76  return [expr {[lsearch -exact {1 true yes on enabled} $value] >= 0}]
77 }
78 
79 ## @brief Map a Hog severity level onto a notification urgency
80 #
81 # @param[in] level The name of the severity level, as passed to Msg
82 #
83 # @returns one of "low", "normal" or "critical"
84 #
85 proc NotifyUrgency {level} {
86  set level [string tolower $level]
87  # Nothing is wrong: status, info and debug
88  if {$level == "status" || $level == "extra_info" || $level == "info" || $level == "debug"} {
89  return "low"
90  } elseif {$level == "error"} {
91  # An error is the only level that stops the build, and most desktops never
92  # auto dismiss a "critical" notification, so it is the only one that gets it
93  return "critical"
94  }
95  # Warnings and critical warnings, and anything unrecognised: worth a look,
96  # but the build is still alive
97  return "normal"
98 }
99 
100 ## @brief Send a desktop notification on Linux through the notify-send helper
101 #
102 # Notifications are opt-in: nothing is sent unless the HOG_NOTIFY environment
103 # variable is enabled, see NotifyEnabled.
104 #
105 # the helper is run detached and silenced: a notification that cannot be delivered,
106 # for example over ssh or with no notification daemon running, is dropped
107 # without interrupting the build.
108 #
109 # @param[in] level The severity level, in any of the forms accepted by Msg
110 # @param[in] message The body of the notification
111 # @param[in] title The title of the notification (default "Hog <level>")
112 #
113 proc Notify {level message {title ""}} {
114  global NotifyInProgress
115 
116  if {![NotifyEnabled] || $NotifyInProgress} {
117  return
118  }
119 
120  if {$title == ""} {
121  set title "Hog [string toupper $level]"
122  }
123 
124  set NotifyInProgress 1
125  set status [catch {
126  if {[auto_execok notify-send] == ""} {
127  Msg Debug "notify-send was not found, skipping notification"
128  } else {
129  set cmd [list notify-send --app-name=Hog --urgency=[NotifyUrgency $level]]
130  set icon [file join $::HogImagesPath hog.png]
131  if {[file exists $icon]} {
132  lappend cmd --icon=$icon
133  }
134  # "--" protects titles and messages that begin with a dash
135  lappend cmd -- $title $message
136  # Vivado and Quartus ship their own glib and put it on LD_LIBRARY_PATH,
137  # Drop it inside an IDE, the same workaround GetCurl uses for curl.
138  if {[info procs IsTclsh] != "" && ![IsTclsh]} {
139  set cmd [linsert $cmd 0 env -u LD_LIBRARY_PATH]
140  }
141  exec {*}$cmd < /dev/null > /dev/null 2> /dev/null &
142  }
143  } err]
144  set NotifyInProgress 0
145 
146  # Reporting the failure must not become a failure of its own
147  if {$status} {
148  catch {Msg Debug "Could not send notification: $err"}
149  }
150 }
151 
152 ## @brief The Hog Printout Msg function
153 #
154 # @param[in] level The severity level (status, info, warning, critical, error, debug)
155 # @param[in] msg The message to print
156 # @param[in] title The title string to be included in the header of the message [Hog:$title] (default "")
157 proc Msg {level fmsg {title ""}} {
158  # foreach msg [split $fmsg "\n"] {
159  set msg $fmsg
160  set level [string tolower $level]
161  if {$title == ""} {set title [lindex [info level [expr {[info level] - 1}]] 0]}
162  if {$level == 0 || $level == "status" || $level == "extra_info"} {
163  set vlevel {STATUS}
164  set qlevel info
165  } elseif {$level == 1 || $level == "info"} {
166  set vlevel {INFO}
167  set qlevel info
168  } elseif {$level == 2 || $level == "warning"} {
169  set vlevel {WARNING}
170  set qlevel warning
171  } elseif {$level == 3 || [string first "critical" $level] != -1} {
172  set vlevel {CRITICAL WARNING}
173  set qlevel critical_warning
174  } elseif {$level == 4 || $level == "error"} {
175  set vlevel {ERROR}
176  set qlevel error
177  } elseif {$level == 5 || $level == "debug"} {
178  if {([info exists ::DEBUG_MODE] && $::DEBUG_MODE == 1) || (
179  [info exists ::env(HOG_DEBUG_MODE)] && $::env(HOG_DEBUG_MODE) == 1
180  )} {
181  set vlevel {STATUS}
182  set qlevel extra_info
183  set msg "DEBUG: \[Hog:$title\] $msg"
184  } else {
185  return
186  }
187  } else {
188  puts "Hog Error: level $level not defined"
189  exit -1
190  }
191  # Every branch below exits on an error, so notify the desktop while we still can
192  if {$qlevel == "error"} {
193  Notify $level $msg "Hog $vlevel"
194  }
195  if {[IsXilinx]} {
196  # Vivado
197  if {[string match "-*" $msg]} {
198  set msg " $msg"
199  }
200  set status [catch {send_msg_id Hog:$title-0 $vlevel "$msg"}]
201  if {$status != 0} {
202  exit $status
203  }
204  } elseif {[IsQuartus]} {
205  # Quartus
206  post_message -type $qlevel "Hog:$title $msg"
207  if {$qlevel == "error"} {
208  exit 1
209  }
210  } else {
211  # Tcl Shell / Libero
212  if {$vlevel != "STATUS"} {
213  puts "$vlevel: \[Hog:$title\] $msg"
214  } else {
215  # temporary solution to avoid removing of leading
216  set HogEnvDict [Hog::LoggerLib::GetTOMLDict]
217  puts "$msg"
218  }
219  if {$qlevel == "error"} {
220  exit 1
221  }
222  }
223  # }
224 }
225 
226 ## @brief Prints a message with selected severity and optionally write into a log file
227 #
228 # @param[in] msg The message to print
229 # @param[in] severity The severity of the message
230 # @param[in] outFile The path of the output logfile
231 #
232 proc MsgAndLog {msg {severity "CriticalWarning"} {outFile ""}} {
233  Msg $severity $msg
234  if {$outFile != ""} {
235  set directory [file dir $outFile]
236  if {![file exists $directory]} {
237  Msg Info "Creating $directory..."
238  file mkdir $directory
239  }
240 
241  set oF [open "$outFile" a+]
242  puts $oF $msg
243  close $oF
244  }
245 }
246 
247 
248 # @brief Print the Hog Logo
249 #
250 # @param[in] repo_path The main path of the git repository (default .)
251 proc Logo {{repo_path .}} {
252  # Msg Warning "HOG_LOGO_PRINTED : $HOG_LOGO_PRINTED"
253  if {![info exists ::env(HOG_LOGO_PRINTED)] || $::env(HOG_LOGO_PRINTED) eq "0"} {
254  if {
255  [info exists ::env(HOG_COLOR)] && ([string match "ENABLED" $::env(HOG_COLOR)] || [string is integer -strict $::env(HOG_COLOR)] && $::env(HOG_COLOR) > 0)
256  } {
257  set logo_file "$repo_path/Hog/images/hog_logo_color.txt"
258  } else {
259  set logo_file "$repo_path/Hog/images/hog_logo.txt"
260  }
261 
262  cd $repo_path/Hog
263  set ver [Git {describe --always}]
264  set old_path [pwd]
265  # set ver [Git {describe --always}]
266 
267  if {[file exists $logo_file]} {
268  set f [open $logo_file "r"]
269  set data [read $f]
270  close $f
271  set lines [split $data "\n"]
272  foreach l $lines {
273  if {[regexp {(Version:)[ ]+} $l -> prefix]} {
274  set string_len [string length $l]
275 
276  set version_string "* Version: $ver"
277  set version_len [string length $version_string]
278  append version_string [string repeat " " [expr {$string_len - $version_len - 1}]] "*"
279  set l $version_string
280  }
281  Msg Status $l
282  }
283  } {
284  Msg CriticalWarning "Logo file: $logo_file not found"
285  }
286 
287  Msg Status ""
288  Msg Status " ★ Like Hog? Star us on GitLab: https://gitlab.com/hog-cern/Hog | GitHub: https://github.com/hog-cern/Hog"
289  Msg Status ""
290 
291  # Msg Status "Version: $ver"
292  cd $old_path
293  }
294 }
295 
296 # Define the procedure to print the content of a file
297 #
298 # @param[in] filename The name of the file to read and print
299 #
300 # @brief This procedure opens the file, reads its content, and prints it to the console.
301 proc PrintFileContent {filename} {
302  # Open the file for reading
303  set file [open $filename r]
304 
305  # Read the content of the file
306  set content [read $file]
307 
308  # Close the file
309  close $file
310 
311  # Print the content of the file
312  puts $content
313 }
314 
315 
316 
317 ## Print a tree-like structure of Hog list file content
318 #
319 # @param[in] data the list of lines read from a list file
320 # @param[in] repo_path the path of the repository
321 # @param[in] indentation a string containing a number of spaces to indent the tree
322 proc PrintFileTree {{data} {repo_path} {indentation ""}} {
323  # Msg Debug "PrintFileTree called with data: $data, repo_path: $repo_path, indentation: $indentation"
324  set print_list {}
325  set last_printed ""
326  foreach line $data {
327  if {![regexp {^[\t\s]*$} $line] & ![regexp {^[\t\s]*\#} $line]} {
328  lappend print_list "$line"
329  }
330  }
331  set i 0
332 
333  foreach p $print_list {
334  incr i
335  if {$i == [llength $print_list]} {
336  set pad "└──"
337  } else {
338  set pad "├──"
339  }
340  set file_name [lindex [split $p] 0]
341  if {[file exists [file normalize [lindex [glob -nocomplain $repo_path/$file_name] 0]]]} {
342  set exists ""
343  } else {
344  set exists " !!!!! NOT FOUND !!!!!"
345  }
346 
347  Msg Status "$indentation$pad$p$exists"
348  set last_printed $file_name
349  }
350 
351  return $last_printed
352 }
353 
354 
355 
356 
357 namespace eval Hog::LoggerLib {
358 
359  variable toml_dict {}
360  variable fullPath
361 
362  ## @brief gets the full path to the file in the user home folder
363  #
364  # @param[in] filename The name of the file to get the path for
365  #
366  # @returns The full path to the file in the user's home directory, or 0 if file doesn't exist
367  #
368  proc GetUserFilePath {filename} {
369  set homeDir [file normalize ~]
370  set fullPath [file join $homeDir $filename]
371  if {[file exists $fullPath]} {
372  return $fullPath
373  } else {
374  return 0
375  }
376  }
377 
378 
379  ## @brief Parse a TOML format file and return the data as a dictionary
380  #
381  # @param[in] toml_file The path to the TOML file to parse
382  #
383  # @returns A nested dictionary containing the TOML data, or -1 in case of failure
384  #
385  proc ParseTOML {toml_file} {
386  variable toml_dict
387 
388  # set toml_dict [dict create \
389  # terminal [dict create logger 0 colored 0] \
390  # verbose [dict create level 4 pidshow 0 linecounter 0 msgtypeCounter 0] \
391  # ]
392  if {![file exists $toml_file]} {
393  Msg Warning "TOML file $toml_file does not exist"
394  return -1
395  }
396  if {[catch {open $toml_file r} file_handle]} {
397  Msg Error "Cannot open TOML file $toml_file: $file_handle"
398  return -1
399  }
400  # set toml_dict [dict create]
401  set current_section ""
402  set line_number 0
403  set in_multiline_string 0
404  set multiline_buffer ""
405  set multiline_key ""
406  while {[gets $file_handle line] >= 0} {
407  incr line_number
408  # Handle multiline strings
409  if {$in_multiline_string} {
410  if {[string match "*\"\"\"*" $line]} {
411  # End of multiline string
412  set end_pos [string first "\"\"\"" $line]
413  append multiline_buffer [string range $line 0 [expr $end_pos - 1]]
414  if {$current_section eq ""} {
415  dict set toml_dict $multiline_key $multiline_buffer
416  } else {
417  dict set toml_dict $current_section $multiline_key $multiline_buffer
418  }
419  set in_multiline_string 0
420  set multiline_buffer ""
421  set multiline_key ""
422  } else {
423  append multiline_buffer $line "\n"
424  }
425  continue
426  }
427  # Remove comments (but preserve # inside strings)
428  set clean_line ""
429  set in_quotes 0
430  set quote_char ""
431  for {set i 0} {$i < [string length $line]} {incr i} {
432  set char [string index $line $i]
433  if {!$in_quotes && ($char eq "\"" || $char eq "'")} {
434  set in_quotes 1
435  set quote_char $char
436  append clean_line $char
437  } elseif {$in_quotes && $char eq $quote_char} {
438  set in_quotes 0
439  set quote_char ""
440  append clean_line $char
441  } elseif {!$in_quotes && $char eq "#"} {
442  break
443  } else {
444  append clean_line $char
445  }
446  }
447  set line [string trim $clean_line]
448  # Skip empty lines
449  if {$line eq ""} {
450  continue
451  }
452  # Handle section headers [section] or [section.subsection]
453  if {[regexp {^\[([^\]]+)\]$} $line match section_name]} {
454  set current_section $section_name
455  # Initialize section if it doesn't exist
456  if {![dict exists $toml_dict $current_section]} {
457  dict set toml_dict $current_section [dict create]
458  }
459  continue
460  }
461  # Handle key-value pairs
462  if {[regexp {^([^=]+)=(.*)$} $line match raw_key raw_value]} {
463  set key [string trim $raw_key]
464  set value [string trim $raw_value]
465  # Handle multiline strings
466  if {[string match "*\"\"\"*" $value] && ![string match "*\"\"\"*\"\"\"*" $value]} {
467  set start_pos [string first "\"\"\"" $value]
468  set multiline_key $key
469  set multiline_buffer [string range $value [expr $start_pos + 3] end]
470  append multiline_buffer "\n"
471  set in_multiline_string 1
472  continue
473  }
474  # Parse the value
475  set parsed_value [ParseTOMLValue $value]
476  # Handle arrays and nested keys
477  if {[string match "*.*" $key]} {
478  set key_parts [split $key "."]
479  set dict_ref toml_dict
480  if {$current_section ne ""} {
481  lappend dict_ref $current_section
482  }
483  for {set i 0} {$i < [expr [llength $key_parts] - 1]} {incr i} {
484  set part [lindex $key_parts $i]
485  lappend dict_ref $part
486  if {![dict exists {*}$dict_ref]} {
487  dict set {*}$dict_ref [dict create]
488  }
489  }
490  set final_key [lindex $key_parts end]
491  lappend dict_ref $final_key
492  dict set {*}$dict_ref $parsed_value
493  } else {
494  # Simple key
495  if {$current_section eq ""} {
496  dict set toml_dict $key $parsed_value
497  } else {
498  dict set toml_dict $current_section $key $parsed_value
499  }
500  }
501  }
502  }
503  close $file_handle
504  return $toml_dict
505  }
506 
507  ## @brief Parse a TOML value and convert it to appropriate TCL type
508  #
509  # @param[in] value The raw value string from TOML
510  #
511  # @returns The parsed value in appropriate TCL format
512  #
513  proc ParseTOMLValue {value} {
514  set value [string trim $value]
515  # Handle boolean values
516  if {$value eq "true"} {
517  return 1
518  } elseif {$value eq "false"} {
519  return 0
520  }
521  # Handle strings (quoted)
522  if {[regexp {^"(.*)"$} $value match string_content]} {
523  # Handle escape sequences
524  set string_content [string map {\\" \" \\\\ \\ \\n \n \\t \t \\r \r} $string_content]
525  return $string_content
526  } elseif {[regexp {^'(.*)'$} $value match string_content]} {
527  # Single quoted strings (literal)
528  return $string_content
529  }
530  # Handle arrays
531  if {[string match {\[*\]} $value]} {
532  set array_content [string range $value 1 end-1]
533  set array_content [string trim $array_content]
534  if {$array_content eq ""} {
535  return [list]
536  }
537  set elements [list]
538  set current_element ""
539  set bracket_depth 0
540  set in_quotes 0
541  set quote_char ""
542  for {set i 0} {$i < [string length $array_content]} {incr i} {
543  set char [string index $array_content $i]
544  if {!$in_quotes && ($char eq "\"" || $char eq "'")} {
545  set in_quotes 1
546  set quote_char $char
547  append current_element $char
548  } elseif {$in_quotes && $char eq $quote_char} {
549  set in_quotes 0
550  set quote_char ""
551  append current_element $char
552  } elseif {!$in_quotes && $char eq "\["} {
553  incr bracket_depth
554  append current_element $char
555  } elseif {!$in_quotes && $char eq "\]"} {
556  incr bracket_depth -1
557  append current_element $char
558  } elseif {!$in_quotes && $char eq "," && $bracket_depth == 0} {
559  lappend elements [ParseTOMLValue [string trim $current_element]]
560  set current_element ""
561  } else {
562  append current_element $char
563  }
564  }
565  if {$current_element ne ""} {
566  lappend elements [ParseTOMLValue [string trim $current_element]]
567  }
568  return $elements
569  }
570  # Handle numbers (integers and floats)
571  if {[string is integer $value]} {
572  return [expr {int($value)}]
573  } elseif {[string is double $value]} {
574  return [expr {double($value)}]
575  }
576  # Handle dates/times as strings for now
577  if {[regexp {^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}} $value]} {
578  return $value
579  }
580  # Return as string if nothing else matches
581  return $value
582  }
583 
584  ## @brief Get a value from a TOML dictionary using dot notation
585  #
586  # @param[in] toml_dict The dictionary returned by ParseTOML
587  # @param[in] key_path The key path in dot notation (e.g., "section.subsection.key")
588  #
589  # @returns The value if found, or empty string if not found
590  #
591  proc GetTOMLValue {toml_dict key_path} {
592  set key_parts [split $key_path "."]
593  set current_dict $toml_dict
594  foreach part $key_parts {
595  if {[dict exists $current_dict $part]} {
596  set current_dict [dict get $current_dict $part]
597  } else {
598  return ""
599  }
600  }
601  return $current_dict
602  }
603 
604  ## @brief Print a TOML dictionary in a readable format
605  #
606  # @param[in] toml_dict The dictionary to print
607  # @param[in] indent Internal parameter for indentation (default: 0)
608  #
609  proc PrintTOMLDict {toml_dict {indent 0}} {
610  set indent_str [string repeat " " $indent]
611  dict for {key value} $toml_dict {
612  if {[string is list $value] && [llength $value] > 1 && [string is list [lindex $value 0]]} {
613  # This is likely a nested dictionary
614  Msg Debug "${indent_str}${key}:"
615  if {[catch {dict for {subkey subvalue} $value {}} result]} {
616  # Not a dictionary, print as value
617  Msg Debug "${indent_str} $value"
618  } else {
619  PrintTOMLDict $value [expr {$indent + 1}]
620  }
621  } elseif {[string is list $value] && [llength $value] > 0} {
622  # This is an array
623  Msg Debug "${indent_str}${key}: \[list of [llength $value] items\]"
624  foreach item $value {
625  Msg Debug "${indent_str} - $item"
626  }
627  } else {
628  Msg Debug "${indent_str}${key}: $value"
629  }
630  }
631  }
632 
633  ## @brief Access the dictionary of the parsed TOML file
634  #
635  # @returns The dictionary containing the parsed TOML data
636  proc GetTOMLDict {} {
637  variable toml_dict
638  if {[info exists toml_dict]} {
639  return $toml_dict
640  }
641  }
642 
643 
644 
645 }
646 
647