Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
1import time
2import html
3import os
4import json
5import sys
6import itertools
7
8from Plugin import PluginManager
9from Config import config
10from util import helper
11from Debug import Debug
12from Db import Db
13
14
15@PluginManager.registerTo("UiRequest")
16class UiRequestPlugin(object):
17
18 def formatTableRow(self, row, class_name=""):
19 back = []
20 for format, val in row:
21 if val is None:
22 formatted = "n/a"
23 elif format == "since":
24 if val:
25 formatted = "%.0f" % (time.time() - val)
26 else:
27 formatted = "n/a"
28 else:
29 formatted = format % val
30 back.append("<td>%s</td>" % formatted)
31 return "<tr class='%s'>%s</tr>" % (class_name, "".join(back))
32
33 def getObjSize(self, obj, hpy=None):
34 if hpy:
35 return float(hpy.iso(obj).domisize) / 1024
36 else:
37 return 0
38
39 def renderHead(self):
40 import main
41 from Crypt import CryptConnection
42
43 # Memory
44 yield "rev%s | " % config.rev
45 yield "%s | " % main.file_server.ip_external_list
46 yield "Port: %s | " % main.file_server.port
47 yield "Network: %s | " % main.file_server.supported_ip_types
48 yield "Opened: %s | " % main.file_server.port_opened
49 yield "Crypt: %s, TLSv1.3: %s | " % (CryptConnection.manager.crypt_supported, CryptConnection.ssl.HAS_TLSv1_3)
50 yield "In: %.2fMB, Out: %.2fMB | " % (
51 float(main.file_server.bytes_recv) / 1024 / 1024,
52 float(main.file_server.bytes_sent) / 1024 / 1024
53 )
54 yield "Peerid: %s | " % main.file_server.peer_id
55 yield "Time: %.2fs | " % main.file_server.getTimecorrection()
56 yield "Blocks: %s" % Debug.num_block
57
58 try:
59 import psutil
60 process = psutil.Process(os.getpid())
61 mem = process.get_memory_info()[0] / float(2 ** 20)
62 yield "Mem: %.2fMB | " % mem
63 yield "Threads: %s | " % len(process.threads())
64 yield "CPU: usr %.2fs sys %.2fs | " % process.cpu_times()
65 yield "Files: %s | " % len(process.open_files())
66 yield "Sockets: %s | " % len(process.connections())
67 yield "Calc size <a href='?size=1'>on</a> <a href='?size=0'>off</a>"
68 except Exception:
69 pass
70 yield "<br>"
71
72 def renderConnectionsTable(self):
73 import main
74
75 # Connections
76 yield "<b>Connections</b> (%s, total made: %s, in: %s, out: %s):<br>" % (
77 len(main.file_server.connections), main.file_server.last_connection_id,
78 main.file_server.num_incoming, main.file_server.num_outgoing
79 )
80 yield "<table class='connections'><tr> <th>id</th> <th>type</th> <th>ip</th> <th>open</th> <th>crypt</th> <th>ping</th>"
81 yield "<th>buff</th> <th>bad</th> <th>idle</th> <th>open</th> <th>delay</th> <th>cpu</th> <th>out</th> <th>in</th> <th>last sent</th>"
82 yield "<th>wait</th> <th>version</th> <th>time</th> <th>sites</th> </tr>"
83 for connection in main.file_server.connections:
84 if "cipher" in dir(connection.sock):
85 cipher = connection.sock.cipher()[0]
86 tls_version = connection.sock.version()
87 else:
88 cipher = connection.crypt
89 tls_version = ""
90 if "time" in connection.handshake and connection.last_ping_delay:
91 time_correction = connection.handshake["time"] - connection.handshake_time - connection.last_ping_delay
92 else:
93 time_correction = 0.0
94 yield self.formatTableRow([
95 ("%3d", connection.id),
96 ("%s", connection.type),
97 ("%s:%s", (connection.ip, connection.port)),
98 ("%s", connection.handshake.get("port_opened")),
99 ("<span title='%s %s'>%s</span>", (cipher, tls_version, connection.crypt)),
100 ("%6.3f", connection.last_ping_delay),
101 ("%s", connection.incomplete_buff_recv),
102 ("%s", connection.bad_actions),
103 ("since", max(connection.last_send_time, connection.last_recv_time)),
104 ("since", connection.start_time),
105 ("%.3f", max(-1, connection.last_sent_time - connection.last_send_time)),
106 ("%.3f", connection.cpu_time),
107 ("%.0fk", connection.bytes_sent / 1024),
108 ("%.0fk", connection.bytes_recv / 1024),
109 ("<span title='Recv: %s'>%s</span>", (connection.last_cmd_recv, connection.last_cmd_sent)),
110 ("%s", list(connection.waiting_requests.keys())),
111 ("%s r%s", (connection.handshake.get("version"), connection.handshake.get("rev", "?"))),
112 ("%.2fs", time_correction),
113 ("%s", connection.sites)
114 ])
115 yield "</table>"
116
117 def renderTrackers(self):
118 # Trackers
119 yield "<br><br><b>Trackers:</b><br>"
120 yield "<table class='trackers'><tr> <th>address</th> <th>request</th> <th>successive errors</th> <th>last_request</th></tr>"
121 from Site import SiteAnnouncer # importing at the top of the file breaks plugins
122 for tracker_address, tracker_stat in sorted(SiteAnnouncer.global_stats.items()):
123 yield self.formatTableRow([
124 ("%s", tracker_address),
125 ("%s", tracker_stat["num_request"]),
126 ("%s", tracker_stat["num_error"]),
127 ("%.0f min ago", min(999, (time.time() - tracker_stat["time_request"]) / 60))
128 ])
129 yield "</table>"
130
131 if "AnnounceShare" in PluginManager.plugin_manager.plugin_names:
132 yield "<br><br><b>Shared trackers:</b><br>"
133 yield "<table class='trackers'><tr> <th>address</th> <th>added</th> <th>found</th> <th>latency</th> <th>successive errors</th> <th>last_success</th></tr>"
134 from AnnounceShare import AnnounceSharePlugin
135 for tracker_address, tracker_stat in sorted(AnnounceSharePlugin.tracker_storage.getTrackers().items()):
136 yield self.formatTableRow([
137 ("%s", tracker_address),
138 ("%.0f min ago", min(999, (time.time() - tracker_stat["time_added"]) / 60)),
139 ("%.0f min ago", min(999, (time.time() - tracker_stat.get("time_found", 0)) / 60)),
140 ("%.3fs", tracker_stat["latency"]),
141 ("%s", tracker_stat["num_error"]),
142 ("%.0f min ago", min(999, (time.time() - tracker_stat["time_success"]) / 60)),
143 ])
144 yield "</table>"
145
146 def renderTor(self):
147 import main
148 yield "<br><br><b>Tor hidden services (status: %s):</b><br>" % main.file_server.tor_manager.status
149 for site_address, onion in list(main.file_server.tor_manager.site_onions.items()):
150 yield "- %-34s: %s<br>" % (site_address, onion)
151
152 def renderDbStats(self):
153 yield "<br><br><b>Db</b>:<br>"
154 for db in Db.opened_dbs:
155 tables = [row["name"] for row in db.execute("SELECT name FROM sqlite_master WHERE type = 'table'").fetchall()]
156 table_rows = {}
157 for table in tables:
158 table_rows[table] = db.execute("SELECT COUNT(*) AS c FROM %s" % table).fetchone()["c"]
159 db_size = os.path.getsize(db.db_path) / 1024.0 / 1024.0
160 yield "- %.3fs: %s %.3fMB, table rows: %s<br>" % (
161 time.time() - db.last_query_time, db.db_path, db_size, json.dumps(table_rows, sort_keys=True)
162 )
163
164 def renderSites(self):
165 yield "<br><br><b>Sites</b>:"
166 yield "<table>"
167 yield "<tr><th>address</th> <th>connected</th> <th title='connected/good/total'>peers</th> <th>content.json</th> <th>out</th> <th>in</th> </tr>"
168 for site in list(self.server.sites.values()):
169 yield self.formatTableRow([
170 (
171 """<a href='#' onclick='document.getElementById("peers_%s").style.display="initial"; return false'>%s</a>""",
172 (site.address, site.address)
173 ),
174 ("%s", [peer.connection.id for peer in list(site.peers.values()) if peer.connection and peer.connection.connected]),
175 ("%s/%s/%s", (
176 len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected]),
177 len(site.getConnectablePeers(100)),
178 len(site.peers)
179 )),
180 ("%s (loaded: %s)", (
181 len(site.content_manager.contents),
182 len([key for key, val in dict(site.content_manager.contents).items() if val])
183 )),
184 ("%.0fk", site.settings.get("bytes_sent", 0) / 1024),
185 ("%.0fk", site.settings.get("bytes_recv", 0) / 1024),
186 ], "serving-%s" % site.settings["serving"])
187 yield "<tr><td id='peers_%s' style='display: none; white-space: pre' colspan=6>" % site.address
188 for key, peer in list(site.peers.items()):
189 if peer.time_found:
190 time_found = int(time.time() - peer.time_found) / 60
191 else:
192 time_found = "--"
193 if peer.connection:
194 connection_id = peer.connection.id
195 else:
196 connection_id = None
197 if site.content_manager.has_optional_files:
198 yield "Optional files: %4s " % len(peer.hashfield)
199 time_added = (time.time() - peer.time_added) / (60 * 60 * 24)
200 yield "(#%4s, rep: %2s, err: %s, found: %.1fs min, add: %.1f day) %30s -<br>" % (connection_id, peer.reputation, peer.connection_error, time_found, time_added, key)
201 yield "<br></td></tr>"
202 yield "</table>"
203
204 def renderBigfiles(self):
205 yield "<br><br><b>Big files</b>:<br>"
206 for site in list(self.server.sites.values()):
207 if not site.settings.get("has_bigfile"):
208 continue
209 bigfiles = {}
210 yield """<a href="#" onclick='document.getElementById("bigfiles_%s").style.display="initial"; return false'>%s</a><br>""" % (site.address, site.address)
211 for peer in list(site.peers.values()):
212 if not peer.time_piecefields_updated:
213 continue
214 for sha512, piecefield in peer.piecefields.items():
215 if sha512 not in bigfiles:
216 bigfiles[sha512] = []
217 bigfiles[sha512].append(peer)
218
219 yield "<div id='bigfiles_%s' style='display: none'>" % site.address
220 for sha512, peers in bigfiles.items():
221 yield "<br> - " + sha512 + " (hash id: %s)<br>" % site.content_manager.hashfield.getHashId(sha512)
222 yield "<table>"
223 for peer in peers:
224 yield "<tr><td>" + peer.key + "</td><td>" + peer.piecefields[sha512].tostring() + "</td></tr>"
225 yield "</table>"
226 yield "</div>"
227
228 def renderRequests(self):
229 import main
230 yield "<div style='float: left'>"
231 yield "<br><br><b>Sent commands</b>:<br>"
232 yield "<table>"
233 for stat_key, stat in sorted(main.file_server.stat_sent.items(), key=lambda i: i[1]["bytes"], reverse=True):
234 yield "<tr><td>%s</td><td style='white-space: nowrap'>x %s =</td><td>%.0fkB</td></tr>" % (stat_key, stat["num"], stat["bytes"] / 1024)
235 yield "</table>"
236 yield "</div>"
237
238 yield "<div style='float: left; margin-left: 20%; max-width: 50%'>"
239 yield "<br><br><b>Received commands</b>:<br>"
240 yield "<table>"
241 for stat_key, stat in sorted(main.file_server.stat_recv.items(), key=lambda i: i[1]["bytes"], reverse=True):
242 yield "<tr><td>%s</td><td style='white-space: nowrap'>x %s =</td><td>%.0fkB</td></tr>" % (stat_key, stat["num"], stat["bytes"] / 1024)
243 yield "</table>"
244 yield "</div>"
245 yield "<div style='clear: both'></div>"
246
247 def renderMemory(self):
248 import gc
249 from Ui import UiRequest
250
251 hpy = None
252 if self.get.get("size") == "1": # Calc obj size
253 try:
254 import guppy
255 hpy = guppy.hpy()
256 except Exception:
257 pass
258 self.sendHeader()
259
260 # Object types
261
262 obj_count = {}
263 for obj in gc.get_objects():
264 obj_type = str(type(obj))
265 if obj_type not in obj_count:
266 obj_count[obj_type] = [0, 0]
267 obj_count[obj_type][0] += 1 # Count
268 obj_count[obj_type][1] += float(sys.getsizeof(obj)) / 1024 # Size
269
270 yield "<br><br><b>Objects in memory (types: %s, total: %s, %.2fkb):</b><br>" % (
271 len(obj_count),
272 sum([stat[0] for stat in list(obj_count.values())]),
273 sum([stat[1] for stat in list(obj_count.values())])
274 )
275
276 for obj, stat in sorted(list(obj_count.items()), key=lambda x: x[1][0], reverse=True): # Sorted by count
277 yield " - %.1fkb = %s x <a href=\"/Listobj?type=%s\">%s</a><br>" % (stat[1], stat[0], obj, html.escape(obj))
278
279 # Classes
280
281 class_count = {}
282 for obj in gc.get_objects():
283 obj_type = str(type(obj))
284 if obj_type != "<type 'instance'>":
285 continue
286 class_name = obj.__class__.__name__
287 if class_name not in class_count:
288 class_count[class_name] = [0, 0]
289 class_count[class_name][0] += 1 # Count
290 class_count[class_name][1] += float(sys.getsizeof(obj)) / 1024 # Size
291
292 yield "<br><br><b>Classes in memory (types: %s, total: %s, %.2fkb):</b><br>" % (
293 len(class_count),
294 sum([stat[0] for stat in list(class_count.values())]),
295 sum([stat[1] for stat in list(class_count.values())])
296 )
297
298 for obj, stat in sorted(list(class_count.items()), key=lambda x: x[1][0], reverse=True): # Sorted by count
299 yield " - %.1fkb = %s x <a href=\"/Dumpobj?class=%s\">%s</a><br>" % (stat[1], stat[0], obj, html.escape(obj))
300
301 from greenlet import greenlet
302 objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)]
303 yield "<br>Greenlets (%s):<br>" % len(objs)
304 for obj in objs:
305 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
306
307 from Worker import Worker
308 objs = [obj for obj in gc.get_objects() if isinstance(obj, Worker)]
309 yield "<br>Workers (%s):<br>" % len(objs)
310 for obj in objs:
311 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
312
313 from Connection import Connection
314 objs = [obj for obj in gc.get_objects() if isinstance(obj, Connection)]
315 yield "<br>Connections (%s):<br>" % len(objs)
316 for obj in objs:
317 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
318
319 from socket import socket
320 objs = [obj for obj in gc.get_objects() if isinstance(obj, socket)]
321 yield "<br>Sockets (%s):<br>" % len(objs)
322 for obj in objs:
323 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
324
325 from msgpack import Unpacker
326 objs = [obj for obj in gc.get_objects() if isinstance(obj, Unpacker)]
327 yield "<br>Msgpack unpacker (%s):<br>" % len(objs)
328 for obj in objs:
329 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
330
331 from Site.Site import Site
332 objs = [obj for obj in gc.get_objects() if isinstance(obj, Site)]
333 yield "<br>Sites (%s):<br>" % len(objs)
334 for obj in objs:
335 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
336
337 objs = [obj for obj in gc.get_objects() if isinstance(obj, self.server.log.__class__)]
338 yield "<br>Loggers (%s):<br>" % len(objs)
339 for obj in objs:
340 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj.name)))
341
342 objs = [obj for obj in gc.get_objects() if isinstance(obj, UiRequest)]
343 yield "<br>UiRequests (%s):<br>" % len(objs)
344 for obj in objs:
345 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
346
347 from Peer import Peer
348 objs = [obj for obj in gc.get_objects() if isinstance(obj, Peer)]
349 yield "<br>Peers (%s):<br>" % len(objs)
350 for obj in objs:
351 yield " - %.1fkb: %s<br>" % (self.getObjSize(obj, hpy), html.escape(repr(obj)))
352
353 objs = [(key, val) for key, val in sys.modules.items() if val is not None]
354 objs.sort()
355 yield "<br>Modules (%s):<br>" % len(objs)
356 for module_name, module in objs:
357 yield " - %.3fkb: %s %s<br>" % (self.getObjSize(module, hpy), module_name, html.escape(repr(module)))
358
359 # /Stats entry point
360 @helper.encodeResponse
361 def actionStats(self):
362 import gc
363
364 self.sendHeader()
365
366 if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
367 yield "This function is disabled on this proxy"
368 return
369
370 s = time.time()
371
372 # Style
373 yield """
374 <style>
375 * { font-family: monospace }
376 table td, table th { text-align: right; padding: 0px 10px }
377 .connections td { white-space: nowrap }
378 .serving-False { opacity: 0.3 }
379 </style>
380 """
381
382 renderers = [
383 self.renderHead(),
384 self.renderConnectionsTable(),
385 self.renderTrackers(),
386 self.renderTor(),
387 self.renderDbStats(),
388 self.renderSites(),
389 self.renderBigfiles(),
390 self.renderRequests()
391
392 ]
393
394 for part in itertools.chain(*renderers):
395 yield part
396
397 if config.debug:
398 for part in self.renderMemory():
399 yield part
400
401 gc.collect() # Implicit grabage collection
402 yield "Done in %.1f" % (time.time() - s)
403
404 @helper.encodeResponse
405 def actionDumpobj(self):
406
407 import gc
408 import sys
409
410 self.sendHeader()
411
412 if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
413 yield "This function is disabled on this proxy"
414 return
415
416 # No more if not in debug mode
417 if not config.debug:
418 yield "Not in debug mode"
419 return
420
421 class_filter = self.get.get("class")
422
423 yield """
424 <style>
425 * { font-family: monospace; white-space: pre }
426 table * { text-align: right; padding: 0px 10px }
427 </style>
428 """
429
430 objs = gc.get_objects()
431 for obj in objs:
432 obj_type = str(type(obj))
433 if obj_type != "<type 'instance'>" or obj.__class__.__name__ != class_filter:
434 continue
435 yield "%.1fkb %s... " % (float(sys.getsizeof(obj)) / 1024, html.escape(str(obj)))
436 for attr in dir(obj):
437 yield "- %s: %s<br>" % (attr, html.escape(str(getattr(obj, attr))))
438 yield "<br>"
439
440 gc.collect() # Implicit grabage collection
441
442 @helper.encodeResponse
443 def actionListobj(self):
444
445 import gc
446 import sys
447
448 self.sendHeader()
449
450 if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
451 yield "This function is disabled on this proxy"
452 return
453
454 # No more if not in debug mode
455 if not config.debug:
456 yield "Not in debug mode"
457 return
458
459 type_filter = self.get.get("type")
460
461 yield """
462 <style>
463 * { font-family: monospace; white-space: pre }
464 table * { text-align: right; padding: 0px 10px }
465 </style>
466 """
467
468 yield "Listing all %s objects in memory...<br>" % html.escape(type_filter)
469
470 ref_count = {}
471 objs = gc.get_objects()
472 for obj in objs:
473 obj_type = str(type(obj))
474 if obj_type != type_filter:
475 continue
476 refs = [
477 ref for ref in gc.get_referrers(obj)
478 if hasattr(ref, "__class__") and
479 ref.__class__.__name__ not in ["list", "dict", "function", "type", "frame", "WeakSet", "tuple"]
480 ]
481 if not refs:
482 continue
483 try:
484 yield "%.1fkb <span title=\"%s\">%s</span>... " % (
485 float(sys.getsizeof(obj)) / 1024, html.escape(str(obj)), html.escape(str(obj)[0:100].ljust(100))
486 )
487 except Exception:
488 continue
489 for ref in refs:
490 yield " ["
491 if "object at" in str(ref) or len(str(ref)) > 100:
492 yield str(ref.__class__.__name__)
493 else:
494 yield str(ref.__class__.__name__) + ":" + html.escape(str(ref))
495 yield "] "
496 ref_type = ref.__class__.__name__
497 if ref_type not in ref_count:
498 ref_count[ref_type] = [0, 0]
499 ref_count[ref_type][0] += 1 # Count
500 ref_count[ref_type][1] += float(sys.getsizeof(obj)) / 1024 # Size
501 yield "<br>"
502
503 yield "<br>Object referrer (total: %s, %.2fkb):<br>" % (len(ref_count), sum([stat[1] for stat in list(ref_count.values())]))
504
505 for obj, stat in sorted(list(ref_count.items()), key=lambda x: x[1][0], reverse=True)[0:30]: # Sorted by count
506 yield " - %.1fkb = %s x %s<br>" % (stat[1], stat[0], html.escape(str(obj)))
507
508 gc.collect() # Implicit grabage collection
509
510 @helper.encodeResponse
511 def actionGcCollect(self):
512 import gc
513 self.sendHeader()
514 yield str(gc.collect())
515
516 # /About entry point
517 @helper.encodeResponse
518 def actionEnv(self):
519 import main
520
521 self.sendHeader()
522
523 yield """
524 <style>
525 * { font-family: monospace; white-space: pre; }
526 h2 { font-size: 100%; margin-bottom: 0px; }
527 small { opacity: 0.5; }
528 table { border-collapse: collapse; }
529 td { padding-right: 10px; }
530 </style>
531 """
532
533 if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
534 yield "This function is disabled on this proxy"
535 return
536
537 yield from main.actions.testEnv(format="html")
538
539
540@PluginManager.registerTo("Actions")
541class ActionsPlugin:
542 def formatTable(self, *rows, format="text"):
543 if format == "html":
544 return self.formatTableHtml(*rows)
545 else:
546 return self.formatTableText(*rows)
547
548 def formatHead(self, title, format="text"):
549 if format == "html":
550 return "<h2>%s</h2>" % title
551 else:
552 return "\n* %s\n" % title
553
554 def formatTableHtml(self, *rows):
555 yield "<table>"
556 for row in rows:
557 yield "<tr>"
558 for col in row:
559 yield "<td>%s</td>" % html.escape(str(col))
560 yield "</tr>"
561 yield "</table>"
562
563 def formatTableText(self, *rows):
564 for row in rows:
565 yield " "
566 for col in row:
567 yield " " + str(col)
568 yield "\n"
569
570 def testEnv(self, format="text"):
571 import gevent
572 import msgpack
573 import pkg_resources
574 import importlib
575 import coincurve
576 import sqlite3
577 from Crypt import CryptBitcoin
578
579 yield "\n"
580
581 yield from self.formatTable(
582 ["ZeroNet version:", "%s rev%s" % (config.version, config.rev)],
583 ["Python:", "%s" % sys.version],
584 ["Platform:", "%s" % sys.platform],
585 ["Crypt verify lib:", "%s" % CryptBitcoin.lib_verify_best],
586 ["OpenSSL:", "%s" % CryptBitcoin.sslcrypto.ecc.get_backend()],
587 ["Libsecp256k1:", "%s" % type(coincurve._libsecp256k1.lib).__name__],
588 ["SQLite:", "%s, API: %s" % (sqlite3.sqlite_version, sqlite3.version)],
589 format=format
590 )
591
592
593 yield self.formatHead("Libraries:")
594 rows = []
595 for lib_name in ["gevent", "greenlet", "msgpack", "base58", "merkletools", "rsa", "socks", "pyasn1", "gevent_ws", "websocket", "maxminddb"]:
596 try:
597 module = importlib.import_module(lib_name)
598 if "__version__" in dir(module):
599 version = module.__version__
600 elif "version" in dir(module):
601 version = module.version
602 else:
603 version = "unknown version"
604
605 if type(version) is tuple:
606 version = ".".join(map(str, version))
607
608 rows.append(["- %s:" % lib_name, version, "at " + module.__file__])
609 except Exception as err:
610 rows.append(["! Error importing %s:", repr(err)])
611
612 """
613 try:
614 yield " - %s<br>" % html.escape(repr(pkg_resources.get_distribution(lib_name)))
615 except Exception as err:
616 yield " ! %s<br>" % html.escape(repr(err))
617 """
618
619 yield from self.formatTable(*rows, format=format)
620
621 yield self.formatHead("Library config:", format=format)
622
623 yield from self.formatTable(
624 ["- gevent:", gevent.config.loop.__module__],
625 ["- msgpack unpacker:", msgpack.Unpacker.__module__],
626 format=format
627 )