2 Storage class for P2P logging information.
4 Built on previous work by Adriana Draghici and Razvan Deaconescu.
6 2011, Razvan Deaconescu, razvan.deaconescu@cs.pub.ro
7 2011, Mariana Marasoiu, mariana.marasoiu@gmail.com
19 # Logging code heavily inspired by Logging HOWTO documentation:
20 # http://docs.python.org/dev/howto/logging.html#configuring-logging
23 # Create logger; default logging level is DEBUG.
24 logger = logging.getLogger(__name__)
25 logger.setLevel(logging.DEBUG)
27 # Create console handler and set level to ERROR.
28 ch = logging.StreamHandler()
29 ch.setLevel(logging.DEBUG)
32 formatter = logging.Formatter('%(filename)s:%(lineno)s - '
33 '%(levelname)s: %(message)s')
35 # Add formatter to console handler.
36 ch.setFormatter(formatter)
38 # Add console handler to logger.
43 'CHOKE': {'id': 1, 'parameters': None},
44 'UNCHOKE': {'id': 2, 'parameters': None},
45 'INTERESTED': {'id': 3, 'parameters': None},
46 'NOT_INTERESTED': {'id': 4, 'parameters': None},
47 'HAVE': {'id': 5, 'parameters': None},
48 'BITFIELD': {'id': 6, 'parameters': None},
49 'REQUEST': {'id': 7, 'parameters': None},
50 'PIECE': {'id': 8, 'parameters': None},
51 'CANCEL': {'id': 9, 'parameters': None},
52 'DHT_PORT': {'id': 10, 'parameters': None}
55 bittorrent_clients = {
59 'url': 'http://www.tribler.org/trac',
61 'streaming_support': True,
68 'url': 'https://trac.p2p-next.org/',
70 'streaming_support': True,
74 'libtorrent-rasterbar': {
77 'url': 'http://www.rasterbar.com/products/libtorrent/',
79 'streaming_support': True,
86 'url': 'http://www.vuze.com/',
88 'streaming_support': True,
95 'url': 'http://www.transmissionbt.com/',
97 'streaming_support': False,
104 'url': 'http://aria2.sourceforge.net/',
106 'streaming_support': False,
112 'language': 'Python',
113 'url': 'http://www.bittorrent.com/',
115 'streaming_support': False,
121 transfer_directions = {
127 """ Class mimics a C structure. """
128 def __init__(self, torrent_filename=None, data_size=None,
130 self.torrent_filename = torrent_filename
131 self.data_size = data_size
132 self.description = description
134 class ClientSession(object):
135 """ Class mimics a C structure. """
136 # TODO: Add timezone.
137 def __init__(self, swarm_id=None, btclient=None, system_os=None,
138 system_os_version=None, system_ram=None, system_cpu=None,
139 public_ip=None, public_port=None, ds_limit=None, us_limit=None,
140 start_time=None, dht_enabled=None, pxe_enabled=None,
141 streaming_enabled=None, features=None, description=None):
142 self.swarm_id = swarm_id
143 self.btclient = btclient
144 self.system_os = system_os
145 self.system_os_version = system_os_version
146 self.system_ram = system_ram
147 self.system_cpu = system_cpu
148 self.public_ip = public_ip
149 self.public_port = public_port
150 self.ds_limit = ds_limit
151 self.us_limit = us_limit
152 self.start_time = start_time
153 self.dht_enabled = dht_enabled
154 self.pxe_enabled = pxe_enabled
155 self.streaming_enabled = streaming_enabled
156 self.features = features
157 self.description = description
159 class PeerStatusMessage(object):
160 """ Class mimics a C structure. """
161 def __init__(self, swarm_id=None, client_session_id=None, timestamp=None,
162 peer_ip=None, peer_port=None, download_speed=None,
164 self.swarm_id = swarm_id
165 self.client_session_id = client_session_id
166 self.timestamp = timestamp
167 self.peer_ip = peer_ip
168 self.peer_port = peer_port
169 self.download_speed = download_speed
170 self.upload_speed = upload_speed
172 class StatusMessage(object):
173 """ Class mimics a C structure. """
174 def __init__(self, swarm_id=None, client_session_id=None, timestamp=None,
175 time=None, num_peers=None, num_dht_peers=None,
176 download_speed=None, upload_speed=None, download_size=None,
177 upload_size=None, eta=None):
178 self.swarm_id = swarm_id
179 self.client_session_id = client_session_id
180 self.timestamp = timestamp
181 self.num_peers = num_peers
182 self.num_dht_peers = num_dht_peers
183 self.download_speed = download_speed
184 self.upload_speed = upload_speed
185 self.download_size = download_size
186 self.upload_size = upload_size
189 class VerboseMessage(object):
190 """ Class mimics a C structure. """
191 def __init__(self, swarm_id=None, client_session_id=None, timestamp=None,
192 transfer_direction=None, peer_ip=None, peer_port=None,
193 message_type=None, index=None, begin=None, length=None,
195 self.swarm_id = swarm_id
196 self.client_session_id = client_session_id
197 self.timestamp = timestamp
198 self.transfer_direction = transfer_direction
199 self.peer_ip = peer_ip
200 self.peer_port = peer_port
201 self.message_type = message_type
205 self.listen_port = listen_port
208 class SwarmDataAccess(object):
210 """Base class for swarm information storage.
212 Define main operations on storage objects: Swarm, ClientSession,
213 StatusMessage, PeerStatusMessage, VerboseMessage.
214 Each object has four corresponding operations: add, remove, get, update.
220 def add_swarm(self, swarm):
223 def remove_swarm(self):
229 def update_swarm(self):
232 def add_client_session(self, session):
235 def remove_client_session(self):
238 def get_client_session(self):
241 def update_client_session(self):
244 def add_peer_status_message(self, msg):
247 def remove_peer_status_message(self):
250 def get_peer_status_message(self):
253 def update_peer_status_message(self):
256 def add_status_message(self, msg):
259 def remove_status_message(self):
262 def get_status_message(self):
265 def update_status_message(self):
268 def add_verbose_message(self, msg):
271 def remove_verbose_message(self):
274 def get_verbose_message(self):
277 def update_verbose_message(self):
281 class FileAccess(SwarmDataAccess):
283 """Subclass of SwarmDataAccess providing access for file storage."""
285 def __init__(self, path):
286 """Add base_path attribute received as argument."""
287 self.base_path = path
290 def find_last_numeric_subfolder(path):
291 """Find last numeric folder in base_path folder.
293 The last numeric folder is the last swarm_id.
297 pattern = re.compile("[0-9]+")
299 # Browse entries in base_path folder.
300 listing = os.listdir(path)
301 for entry in listing:
302 # If directory name is a number (id) add it to the list.
303 if os.path.isdir(os.path.join(path, entry)):
304 if pattern.match(entry):
305 dir_list.append(int(entry))
311 return dir_list[len(dir_list)-1]
314 class TreeTextFileAccess(FileAccess):
316 """Child class of FileAccess for directory tree structure access."""
318 def __init__(self, path):
319 """Use superclass method for initialisation of constructor."""
320 super(TreeTextFileAccess, self).__init__(path)
322 def add_swarm(self, swarm):
324 Create a subfolder with an unique id. Add 1 to the last numeric
325 subfolder id. In case none exists, use 1 as id.
327 id = find_last_numeric_subfolder(self.base_path)
333 swarm_path = os.path.join(self.base_path, str(id))
336 swarm_config = os.path.join(swarm_path, "swarm.conf")
337 f = open(swarm_config, 'w')
339 torrent_filename = %s
342 """ %(id, swarm.torrent_filename, swarm.data_size, swarm.description))
345 def add_client_session(self, session):
347 Create session subfolder in swarm subfolder and add config file.
348 TODO: Throw exception in case swarm subfolder doesn't exist.
350 swarm_path = os.path.join(self.base_path, str(session.swarm_id))
352 # Search first available folder in swarm_path.
353 id = find_last_numeric_subfolder(swarm_path)
359 # Create session subfolder.
360 session_path = os.path.join(swarm_path, str(id))
361 os.mkdir(session_path)
363 # Create and populate configuration file.
364 session_config = os.path.join(session_path, "client_session.conf")
365 f = open(session_config, 'w')
370 system_os_version = %s
380 streaming_enabled = %s
383 """ %(id, session.swarm_id, session.btclient, session.system_os,
384 session.system_os_version, session.system_ram, session.system_cpu,
385 session.public_ip, session.public_port, session.ds_limit,
386 session.us_limit, session.start_time, session.dht_enabled,
387 session.pxe_enabled, session.streaming_enabled,
388 session.features, session.description))
391 def add_peer_status_message(self, msg):
392 """Add peer status msg line to file. msg type is PeerStatusMessage."""
393 # TODO: id is number of lines in file.
394 swarm_path = os.path.join(self.base_path, str(msg.swarm_id))
395 session_path = os.path.join(swarm_path, str(msg.client_session_id))
396 message_file = os.path.join(session_path, "peer_status.txt")
398 f = open(message_file, 'a')
399 f.write("""%s,%s,%s,%s,%s,%s,%s,%s\n"""
400 %(1, msg.swarm_id, msg.client_session_id, msg.timestamp,
401 msg.peer_ip, msg.peer_port, msg.download_speed,
405 def add_status_message(self, msg):
406 """Add status msg line to file. msg type is StatusMessage."""
407 # TODO: id is number of lines in file.
408 swarm_path = os.path.join(self.base_path, str(msg.swarm_id))
409 session_path = os.path.join(swarm_path, str(msg.client_session_id))
410 message_file = os.path.join(session_path, "status.txt")
412 f = open(message_file, 'a')
413 f.write("""%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"""
414 %(1, msg.swarm_id, msg.client_session_id, msg.timestamp,
415 msg.num_peers, msg.num_dht_peers, msg.download_speed,
416 msg.upload_speed, msg.download_size, msg.upload_size,
420 def add_verbose_message(self, msg):
421 """Add verbose msg line to file. msg type is VerboseMessage."""
422 # TODO: id is number of lines in file.
423 swarm_path = os.path.join(self.base_path, str(msg.swarm_id))
424 session_path = os.path.join(swarm_path, str(msg.client_session_id))
425 message_file = os.path.join(session_path, "verbose.txt")
427 f = open(message_file, 'a')
428 f.write("""%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"""
429 %(1, msg.swarm_id, msg.client_session_id, msg.timestamp,
430 msg.transfer_direction, msg.peer_ip, msg.peer_port,
431 msg.message_type, msg.index, msg.begin, msg.length,
435 class DatabaseAccess(SwarmDataAccess):
437 """Subclass of SwarmDataAccess providing functions for database usage."""
439 def __init__(self, database):
440 """Add database attribute received as argument.
442 The argument may be a name or a list with options for connection.
445 self.database = database
448 """Initialize access attributes."""
452 def disconnect(self):
453 """Close connection with database."""
457 class SQLiteDatabaseAccess(DatabaseAccess):
459 """Child class of DatabaseAccess for SQLite access."""
461 def __init__(self, database):
462 """Use superclass method for initialisation of constructor."""
463 super(SQLiteDatabaseAccess, self).__init__(database)
466 """Connect to sqlite3 database."""
467 self.conn = sqlite3.connect(self.database)
468 self.cursor = self.conn.cursor()
469 # Use foreign key support if available.
470 self.cursor.execute("PRAGMA foreign_keys = ON")
473 def reset_query(self):
477 def append_to_insert_query(self, data_name, data_value):
478 if data_value is not None:
479 self.columns = self.columns + data_name + ", "
480 self.values = self.values + "'" + str(data_value) + "'" + ", "
482 def add_swarm(self, swarm):
483 """Insert swarm in database. swarm type is Swarm."""
485 self.append_to_insert_query("torrent_filename", swarm.torrent_filename)
486 self.append_to_insert_query("data_size", swarm.data_size)
487 self.append_to_insert_query("description", swarm.description)
489 self.columns = re.sub(',\s*$', '', self.columns)
490 self.values = re.sub(',\s*$', '', self.values)
491 insert_query = "INSERT INTO swarms(" + self.columns +")" + \
492 " VALUES(" + self.values + ")"
493 self.cursor.execute(insert_query)
496 def add_client_session(self, session):
497 """Insert session in database. session type is Session."""
499 self.append_to_insert_query("swarm_id", session.swarm_id)
500 # TODO: search database for client ID
501 self.append_to_insert_query("btclient_id",
502 bittorrent_clients[session.btclient]['id'])
503 self.append_to_insert_query("system_os", session.system_os)
504 self.append_to_insert_query("system_os_version",
505 session.system_os_version)
506 self.append_to_insert_query("system_ram", session.system_ram)
507 self.append_to_insert_query("system_cpu", session.system_cpu)
508 self.append_to_insert_query("public_ip", session.public_ip)
509 self.append_to_insert_query("public_port", session.public_port)
510 self.append_to_insert_query("ds_limit", session.ds_limit)
511 self.append_to_insert_query("us_limit", session.us_limit)
512 self.append_to_insert_query("start_time", session.start_time)
513 self.append_to_insert_query("dht_enabled", session.dht_enabled)
514 self.append_to_insert_query("pxe_enabled", session.pxe_enabled)
515 self.append_to_insert_query("streaming_enabled",
516 session.streaming_enabled)
517 self.append_to_insert_query("features", session.features)
518 self.append_to_insert_query("description", session.description)
520 self.columns = re.sub(',\s*$', '', self.columns)
521 self.values = re.sub(',\s*$', '', self.values)
522 insert_query = "INSERT INTO client_sessions(" + self.columns +")" + \
523 " VALUES(" + self.values + ")"
524 self.cursor.execute(insert_query)
527 def get_string_timestamp(self, ts):
528 # Timestamp is Python Datatime. Convert it to string format and
529 # pass it to internal SQLITE julianday() function.
530 return "%s-%s-%s %s:%s:%s.%s" \
531 %(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second,
532 1000 * ts.microsecond)
534 def add_peer_status_message(self, msg):
535 """Insert peer status message in database.
536 msg type is PeerStatusMessage.
539 self.append_to_insert_query("client_session_id", msg.client_session_id)
541 # TODO: Check msg.timestamp is not None. Raise exception.
542 timestamp_string = self.get_string_timestamp(msg.timestamp)
543 value = "julianday(" + timestamp_string + ")"
544 self.append_to_insert_query("timestamp", value)
546 self.append_to_insert_query("timestamp", msg.timestamp)
547 self.append_to_insert_query("peer_ip", msg.peer_ip)
548 self.append_to_insert_query("peer_port", msg.peer_port)
549 self.append_to_insert_query("download_speed", msg.download_speed)
550 self.append_to_insert_query("upload_speed", msg.upload_speed)
552 self.columns = re.sub(',\s*$', '', self.columns)
553 self.values = re.sub(',\s*$', '', self.values)
554 insert_query = "INSERT INTO peer_status_messages(" + \
555 self.columns +")" + " VALUES(" + self.values + ")"
556 self.cursor.execute(insert_query)
559 def add_status_message(self, msg):
560 """Insert status msg in database. msg type is StatusMessage."""
562 self.append_to_insert_query("client_session_id", msg.client_session_id)
564 # TODO: Check msg.timestamp is not None. Raise exception.
565 timestamp_string = self.get_string_timestamp(msg.timestamp)
566 value = "julianday(" + timestamp_string + ")"
567 self.append_to_insert_query("timestamp", value)
569 self.append_to_insert_query("num_peers", msg.num_peers)
570 self.append_to_insert_query("num_dht_peers", msg.num_dht_peers)
571 self.append_to_insert_query("download_speed", msg.download_speed)
572 self.append_to_insert_query("upload_speed", msg.upload_speed)
573 self.append_to_insert_query("download_size", msg.download_size)
574 self.append_to_insert_query("upload_size", msg.upload_size)
575 self.append_to_insert_query("eta", msg.eta)
577 self.columns = re.sub(',\s*$', '', self.columns)
578 self.values = re.sub(',\s*$', '', self.values)
579 insert_query = "INSERT INTO status_messages(" + self.columns +")" + \
580 " VALUES(" + self.values + ")"
581 self.cursor.execute(insert_query)
584 def add_verbose_message(self, msg):
585 """Insert verbose msg in database. msg type is VerboseMessage."""
587 self.append_to_insert_query("client_session_id", msg.client_session_id)
589 # TODO: Check msg.timestamp is not None. Raise exception.
590 timestamp_string = self.get_string_timestamp(msg.timestamp)
591 value = "julianday(" + timestamp_string + ")"
592 self.append_to_insert_query("timestamp", value)
594 self.append_to_insert_query("transfer_direction_id",
595 transfer_directions[msg.transfer_direction])
596 self.append_to_insert_query("peer_ip", msg.peer_ip)
597 self.append_to_insert_query("peer_port", msg.peer_port)
598 self.append_to_insert_query("message_type_id",
599 message_types[msg.message_type]['id'])
600 self.append_to_insert_query("index", msg.index)
601 self.append_to_insert_query("begin", msg.begin)
602 self.append_to_insert_query("length", msg.length)
603 self.append_to_insert_query("listen_port", msg.listen_port)
605 self.columns = re.sub(',\s*$', '', self.columns)
606 self.values = re.sub(',\s*$', '', self.values)
607 insert_query = "INSERT INTO verbose_messages(" + self.columns +")" + \
608 " VALUES(" + self.values + ")"
609 self.cursor.execute(insert_query)
612 class MySQLDatabaseAccess(DatabaseAccess):
614 """Child class of DatabaseAccess for MySQL access."""
616 def __init__(self, database):
617 """Initialize the database attribute of the class.
619 Use superclass method for initialisation of constructor.
622 super(MySQLDatabaseAccess, self).__init__(database)
625 """Connect to MySQL database."""
626 self.conn = MySQLdb.Connection(db=self.database['database'],
627 user=self.database['user'],
628 passwd=self.database['password'],
629 host=self.database['host'],
630 port=int(self.database['port']))
631 self.cursor = self.conn.cursor()
634 def reset_query(self):
638 def append_to_insert_query(self, data_name, data_value):
639 if data_value is not None:
640 self.columns = self.columns + data_name + ", "
641 self.values = self.values + "'" + str(data_value) + "'" + ", "
643 def add_swarm(self, swarm):
644 """Insert swarm in database. swarm type is Swarm."""
646 self.append_to_insert_query("torrent_filename", swarm.torrent_filename)
647 self.append_to_insert_query("data_size", swarm.data_size)
648 self.append_to_insert_query("description", swarm.description)
650 self.columns = re.sub(',\s*$', '', self.columns)
651 self.values = re.sub(',\s*$', '', self.values)
652 insert_query = "INSERT INTO swarms(" + self.columns +")" + \
653 " VALUES(" + self.values + ")"
654 self.cursor.execute(insert_query)
657 def add_client_session(self, session):
658 """Insert session in database. session type is Session."""
660 self.append_to_insert_query("swarm_id", session.swarm_id)
661 # TODO: search database for client ID
662 self.append_to_insert_query("btclient_id",
663 bittorrent_clients[session.btclient]['id'])
664 self.append_to_insert_query("system_os", session.system_os)
665 self.append_to_insert_query("system_os_version",
666 session.system_os_version)
667 self.append_to_insert_query("system_ram", session.system_ram)
668 self.append_to_insert_query("system_cpu", session.system_cpu)
669 self.append_to_insert_query("public_ip", session.public_ip)
670 self.append_to_insert_query("public_port", session.public_port)
671 self.append_to_insert_query("ds_limit", session.ds_limit)
672 self.append_to_insert_query("us_limit", session.us_limit)
673 self.append_to_insert_query("start_time", session.start_time)
674 self.append_to_insert_query("dht_enabled", session.dht_enabled)
675 self.append_to_insert_query("pxe_enabled", session.pxe_enabled)
676 self.append_to_insert_query("streaming_enabled",
677 session.streaming_enabled)
678 self.append_to_insert_query("features", session.features)
679 self.append_to_insert_query("description", session.description)
681 self.columns = re.sub(',\s*$', '', self.columns)
682 self.values = re.sub(',\s*$', '', self.values)
683 insert_query = "INSERT INTO client_sessions(" + self.columns +")" + \
684 " VALUES(" + self.values + ")"
685 self.cursor.execute(insert_query)
688 def get_string_timestamp(self, ts):
689 # Timestamp is Python Datatime. Convert it to string format.
690 return "%s-%s-%s %s:%s:%s" \
691 %(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second)
693 def add_peer_status_message(self, msg):
694 """Insert peer status message in database.
695 msg type is PeerStatusMessage.
698 self.append_to_insert_query("client_session_id", msg.client_session_id)
700 # TODO: Check msg.timestamp is not None. Raise exception.
701 timestamp_string = self.get_string_timestamp(msg.timestamp)
703 self.append_to_insert_query("timestamp", timestamp_string)
704 self.append_to_insert_query("peer_ip", msg.peer_ip)
705 self.append_to_insert_query("peer_port", msg.peer_port)
706 self.append_to_insert_query("download_speed", msg.download_speed)
707 self.append_to_insert_query("upload_speed", msg.upload_speed)
709 self.columns = re.sub(',\s*$', '', self.columns)
710 self.values = re.sub(',\s*$', '', self.values)
711 insert_query = "INSERT INTO peer_status_messages(" + \
712 self.columns +")" + " VALUES(" + self.values + ")"
713 self.cursor.execute(insert_query)
716 def add_status_message(self, msg):
717 """Insert status msg in database. msg type is StatusMessage."""
719 self.append_to_insert_query("client_session_id", msg.client_session_id)
721 # TODO: Check msg.timestamp is not None. Raise exception.
722 timestamp_string = self.get_string_timestamp(msg.timestamp)
724 self.append_to_insert_query("timestamp", timestamp_string)
725 self.append_to_insert_query("num_peers", msg.num_peers)
726 self.append_to_insert_query("num_dht_peers", msg.num_dht_peers)
727 self.append_to_insert_query("download_speed", msg.download_speed)
728 self.append_to_insert_query("upload_speed", msg.upload_speed)
729 self.append_to_insert_query("download_size", msg.download_size)
730 self.append_to_insert_query("upload_size", msg.upload_size)
731 self.append_to_insert_query("eta", msg.eta)
733 self.columns = re.sub(',\s*$', '', self.columns)
734 self.values = re.sub(',\s*$', '', self.values)
735 insert_query = "INSERT INTO status_messages(" + self.columns +")" + \
736 " VALUES(" + self.values + ")"
737 self.cursor.execute(insert_query)
740 def add_verbose_message(self, msg):
741 """Insert verbose msg in database. msg type is VerboseMessage."""
743 self.append_to_insert_query("client_session_id", msg.client_session_id)
745 # TODO: Check msg.timestamp is not None. Raise exception.
746 timestamp_string = self.get_string_timestamp(msg.timestamp)
748 self.append_to_insert_query("timestamp", timestamp_string)
749 self.append_to_insert_query("transfer_direction_id",
750 transfer_directions[msg.transfer_direction])
751 self.append_to_insert_query("peer_ip", msg.peer_ip)
752 self.append_to_insert_query("peer_port", msg.peer_port)
753 self.append_to_insert_query("message_type_id",
754 message_types[msg.message_type]['id'])
755 self.append_to_insert_query("index", msg.index)
756 self.append_to_insert_query("begin", msg.begin)
757 self.append_to_insert_query("length", msg.length)
758 self.append_to_insert_query("listen_port", msg.listen_port)
760 self.columns = re.sub(',\s*$', '', self.columns)
761 self.values = re.sub(',\s*$', '', self.values)
762 insert_query = "INSERT INTO verbose_messages(" + self.columns +")" + \
763 " VALUES(" + self.values + ")"
764 self.cursor.execute(insert_query)
767 class SwarmWriter(object):
768 """Wrapper class for swarm storage write actions.
770 Multiple *Access objects may be added to the clas resulting in multiple
772 For example, adding a swarm could result in
773 * adding a table entry in a MySQL database (MySQLDatabaseAccess)
774 * adding a table entry in an SQLite database (SQLiteDatabaseAccess)
775 * adding a new folder in a tree structure (TreeTextFileAccess)
781 def add_access_handle(self, handle):
782 self.handlers.append(handle)
784 def remove_access_handle(self, handle):
785 self.handlers.remove(handle)
787 def add_swarm(self, swarm):
788 for h in self.handlers:
791 def add_client_session(self, session):
792 for h in self.handlers:
793 h.add_client_session(session)
795 def add_peer_status_message(self, msg):
796 for h in self.handlers:
797 h.add_peer_status_message(msg)
799 def add_status_message(self, msg):
800 for h in self.handlers:
801 h.add_status_message(msg)
803 def add_verbose_message(self, msg):
804 for h in self.handlers:
805 h.add_verbose_message(msg)