3 from datetime import datetime
8 from threading import Thread
12 from typing import Optional
17 import renderer_catalog
20 import trigger_catalog
24 def filter_news_during_dinnertime(page: str) -> bool:
26 is_dinnertime = now.hour >= 17 and now.hour <= 20
27 return not is_dinnertime or not (
30 or "mynorthwest" in page
38 def thread_janitor() -> None:
40 tracemalloc_target = 0.0
46 if now > tracemalloc_target:
47 tracemalloc_target = now + 30.0
48 snapshot = tracemalloc.take_snapshot()
49 snapshot = snapshot.filter_traces((
50 tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
51 tracemalloc.Filter(False, "<unknown>"),
55 top_stats = snapshot.statistics(key_type)
56 print("janitor: Top %s lines" % limit)
57 for index, stat in enumerate(top_stats[:limit], 1):
58 frame = stat.traceback[0]
59 # replace "/path/to/module/file.py" with "module/file.py"
60 filename = os.sep.join(frame.filename.split(os.sep)[-2:])
61 print("janitor: #%s: %s:%s: %.1f KiB"
62 % (index, filename, frame.lineno, stat.size / 1024))
63 line = linecache.getline(frame.filename, frame.lineno).strip()
65 print('janitor: %s' % line)
67 other = top_stats[limit:]
69 size = sum(stat.size for stat in other)
70 print("janitor: %s other: %.1f KiB" % (len(other), size / 1024))
71 total = sum(stat.size for stat in top_stats)
72 print("janitor: Total allocated size: %.1f KiB" % (total / 1024))
74 print("janitor: Running gc operation")
75 gc_target = now + 60.0
80 def thread_change_current() -> None:
81 page_chooser = chooser.weighted_random_chooser_with_triggers(
82 trigger_catalog.get_triggers(), [filter_news_during_dinnertime]
84 swap_page_target = 0.0
88 (page, triggered) = page_chooser.choose_next_page()
91 print("chooser[%s] - WE ARE TRIGGERED." % utils.timestamp())
94 "chooser[%s] - EMERGENCY PAGE %s LOAD NEEDED"
95 % (utils.timestamp(), page)
98 f = open(os.path.join(constants.pages_dir, "current.shtml"), "w")
102 print("chooser[%s] - page does not exist?!" % (utils.timestamp()))
105 swap_page_target = now + constants.refresh_period_sec
107 # Also notify XMLHTTP clients that they need to refresh now.
108 path = os.path.join(constants.pages_dir, "reload_immediately.html")
110 f.write("Reload, suckers!")
113 # Fix this hack... maybe read the webserver logs and see if it
114 # actually was picked up?
118 elif now >= swap_page_target:
119 if page == last_page:
122 "chooser[%s] - nominal choice got the same page..."
123 % (utils.timestamp())
127 print("chooser[%s] - nominal choice of %s" % (utils.timestamp(), page))
129 f = open(os.path.join(constants.pages_dir, "current.shtml"), "w")
130 emit_wrapped(f, page)
133 print("chooser[%s] - page does not exist?!" % (utils.timestamp()))
136 swap_page_target = now + constants.refresh_period_sec
140 def emit_wrapped(f, filename) -> None:
141 def pick_background_color() -> str:
143 if now.hour <= 6 or now.hour >= 21:
145 elif now.hour == 7 or now.hour == 20:
150 def get_refresh_period() -> float:
153 return constants.refresh_period_night_sec * 1000
155 return constants.refresh_period_sec * 1000
157 age = utils.describe_age_of_file_briefly(f"pages/{filename}")
158 bgcolor = pick_background_color()
162 <TITLE>Kitchen Kiosk</TITLE>
163 <LINK rel="stylesheet" type="text/css" href="style.css">
164 <SCRIPT TYPE="text/javascript">
166 // Zoom the 'contents' div to fit without scrollbars and then make
168 function zoomScreen() {
171 document.getElementById("content").style.zoom = z+"%%";
172 var body = document.body;
173 var html = document.documentElement;
174 var height = Math.max(body.scrollHeight,
179 var windowHeight = window.innerHeight;
180 var width = Math.max(body.scrollWidth,
185 var windowWidth = window.innerWidth;
186 var heightRatio = height / windowHeight;
187 var widthRatio = width / windowWidth;
189 if (heightRatio <= 1.0 && widthRatio <= 1.0) {
194 document.getElementById("content").style.visibility = "visible";
197 // Load IMG tags with DATA-SRC attributes late.
198 function lateLoadImages() {
199 var image = document.getElementsByTagName('img');
200 for (var i = 0; i < image.length; i++) {
201 if (image[i].getAttribute('DATA-SRC')) {
202 image[i].setAttribute('SRC', image[i].getAttribute('DATA-SRC'));
207 // Operate the clock at the top of the page.
208 function runClock() {
209 var today = new Date();
210 var h = today.getHours();
211 var ampm = h >= 12 ? 'pm' : 'am';
213 h = h ? h : 12; // the hour '0' should be '12'
214 var m = maybeAddZero(today.getMinutes());
216 if (today.getSeconds() %% 2 == 0) {
217 colon = "<FONT STYLE='color: #%s; font-size: 4vmin; font-weight: bold'>:</FONT>";
219 document.getElementById("time").innerHTML = h + colon + m + ampm;
220 document.getElementById("date").innerHTML = today.toDateString();
221 var t = setTimeout(function(){runClock()}, 1000);
224 // Helper method for running the clock.
225 function maybeAddZero(x) {
226 return (x < 10) ? "0" + x : x;
229 // Do something on page load.
230 function addLoadEvent(func) {
231 var oldonload = window.onload;
232 if (typeof window.onload != 'function') {
233 window.onload = func;
235 window.onload = function() {
244 // Sleep thread helper.
245 const sleep = (milliseconds) => {
246 return new Promise(resolve => setTimeout(resolve, milliseconds))
249 var loadedDate = new Date();
251 addLoadEvent(zoomScreen);
252 addLoadEvent(runClock);
253 addLoadEvent(lateLoadImages);
255 // Runs the countdown line at the bottom and is responsible for
256 // normal page reloads caused by the expiration of a timer.
257 (function countdown() {
260 var now = new Date();
261 var deltaMs = now.getTime() - loadedDate.getTime();
263 var remainingMs = (totalMs - deltaMs);
265 if (remainingMs > 0) {
266 var hr = document.getElementById("countdown");
267 var width = (remainingMs / (totalMs - 5000)) * 100.0;
269 hr.style.visibility = "visible";
270 hr.style.width = " ".concat(width, "%%");
271 hr.style.backgroundColor = "maroon";
274 // Reload unconditionally after 22 sec.
275 window.location.reload();
278 // Brief sleep before doing it all over again.
279 sleep(50).then(() => {
285 // Periodically checks for emergency reload events.
289 var xhr = new XMLHttpRequest();
291 'http://%s/kiosk/pages/reload_immediately.html');
294 if (xhr.status === 200) {
295 window.location.reload();
297 sleep(500).then(() => {
308 <TABLE style="height:100%%; width:100%%" BORDER=0>
311 <DIV id="date"> </DIV>
313 <TD ALIGN="center"><FONT COLOR=#bbbbbb>
314 <DIV id="info"></DIV></FONT>
317 <DIV id="time"> </DIV>
320 <TR STYLE="vertical-align:top">
322 <DIV ID="content" STYLE="zoom: 1; visibility: hidden;">
323 <!-- BEGIN main page contents. -->
324 <!--#include virtual=\"%s\"-->
325 <!-- END main page contents. -->
328 <DIV STYLE="position: absolute; top:1030px; width:99%%">
330 <FONT SIZE=2 COLOR=#bbbbbb>%s @ %s ago.</FONT>
332 <HR id="countdown" STYLE="width:0px;
339 background-color:#ffffff;">
347 get_refresh_period(),
357 def thread_invoke_renderers() -> None:
359 print(f"renderer[{utils.timestamp()}]: invoking all renderers in catalog...")
360 for r in renderer_catalog.get_renderers():
364 except Exception as e:
365 traceback.print_exc()
367 f"renderer[{utils.timestamp()}] unknown exception in {r.get_name()}, swallowing it."
369 delta = time.time() - now
372 f"renderer[{utils.timestamp()}]: Warning: {r.get_name()}'s rendering took {delta:5.2f}s."
375 f"renderer[{utils.timestamp()}]: thread having a little break for {constants.render_period_sec}s..."
377 time.sleep(constants.render_period_sec)
380 if __name__ == "__main__":
381 logging.basicConfig()
382 changer_thread: Optional[Thread] = None
383 renderer_thread: Optional[Thread] = None
384 janitor_thread: Optional[Thread] = None
386 if changer_thread is None or not changer_thread.is_alive():
388 f"MAIN[{utils.timestamp()}] - (Re?)initializing chooser thread... (wtf?!)"
390 changer_thread = Thread(target=thread_change_current, args=())
391 changer_thread.start()
392 if renderer_thread is None or not renderer_thread.is_alive():
394 f"MAIN[{utils.timestamp()}] - (Re?)initializing render thread... (wtf?!)"
396 renderer_thread = Thread(target=thread_invoke_renderers, args=())
397 renderer_thread.start()
398 if janitor_thread is None or not janitor_thread.is_alive():
400 f"MAIN[{utils.timestamp()}] - (Re?)initializing janitor thread... (wtf?!)"
402 janitor_thread = Thread(target=thread_janitor, args=())
403 janitor_thread.start()