def run(self): while True: # Connect to given server address = (self.server.host, self.server.port) try: self.server_socket = socket.create_connection(address) except (ConnectionRefusedError, socket.gaierror): # Tell controller we failed self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, "Can't connect to %s:%s" % address)) # Try reconnecting in a minute cron.reschedule(self.cron_control_channel, 60, self.control_channel, (controlmessage_types.reconnect,)) # Handle messages reconnect = True while True: command_type, *arguments = self.control_channel.recv() if command_type == controlmessage_types.reconnect: break elif command_type == controlmessage_types.quit: reconnect = False break else: error_message = 'Control message not supported when not connected: %s' % repr((command_type, *arguments)) self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, error_message)) # Remove the reconnect message in case we were told to reconnnect manually cron.delete(self.cron_control_channel, self.control_channel, (controlmessage_types.reconnect,)) if reconnect: continue else: break # Create an API object to give to outside line handler self.api = API(self) try: # Run initialization self.send_line_raw(b'USER %s a a :%s' % (self.server.username.encode('utf-8'), self.server.realname.encode('utf-8'))) # Set up nick self.api.nick(self.server.nick.encode('utf-8')) # Run the on_connect hook, to allow further setup botcmd.on_connect(irc = self.api) # Join channels for channel in self.server.channels: self.api.join(channel.encode('utf-8')) # Schedule a ping to be sent in 3 minutes of no activity cron.reschedule(self.cron_control_channel, 3 * 60, self.control_channel, (controlmessage_types.ping,)) # Run mainloop reconnecting = self.mainloop() if not reconnecting: # Run bot cleanup code botcmd.on_quit(irc = self.api) # Tell the server we're quiting self.send_line_raw(b'QUIT :%s exiting normally' % self.server.username.encode('utf-8')) self.server_socket.close() break else: # Tell server we're reconnecting self.send_line_raw(b'QUIT :Reconnecting') self.server_socket.close() except (BrokenPipeError, TimeoutError) as err: # Connection broke, log it and try to reconnect self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, 'Broken socket/pipe or timeout')) self.server_socket.close() # Tell controller we're quiting self.logging_channel.send((logmessage_types.internal, internal_submessage_types.quit)) # Tell cron we're quiting cron.quit(cron_control_channel)
def mainloop(self): # Register both the server socket and the control channel to a polling object poll = select.poll() poll.register(self.server_socket, select.POLLIN) poll.register(self.control_channel, select.POLLIN) # Keep buffer for input server_input_buffer = bytearray() quitting = False reconnecting = False while not quitting and not reconnecting: # Wait until we can do something for fd, event in poll.poll(): # Server if fd == self.server_socket.fileno(): # Ready to receive, read into buffer and handle full messages if event | select.POLLIN: data = self.server_socket.recv(1024) # Mo data to be read even as POLLIN triggered → connection has broken # Log it and try reconnecting if data == b'': self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, 'Empty read')) reconnecting = True break server_input_buffer.extend(data) # Try to see if we have a full line ending with \r\n in the buffer # If yes, handle it while b'\r\n' in server_input_buffer: # Newline was found, split buffer line, _, server_input_buffer = server_input_buffer.partition(b'\r\n') self.handle_line(line) # Remove possible pending ping timeout timer and reset ping timer to 3 minutes cron.delete(self.cron_control_channel, self.control_channel, (controlmessage_types.ping_timeout,)) cron.reschedule(self.cron_control_channel, 3 * 60, self.control_channel, (controlmessage_types.ping,)) else: error_message = 'Event on server socket: %s' % event self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, error_message)) # Control elif fd == self.control_channel.fileno(): command_type, *arguments = self.control_channel.recv() if command_type == controlmessage_types.quit: quitting = True elif command_type == controlmessage_types.send_line: assert len(arguments) == 1 irc_command, space, arguments = arguments[0].encode('utf-8').partition(b' ') line = irc_command.upper() + space + arguments self.send_line_raw(line) elif command_type == controlmessage_types.ping: assert len(arguments) == 0 self.send_line_raw(b'PING :foo') # Reset ping timeout timer to 2 minutes cron.reschedule(self.cron_control_channel, 2 * 60, self.control_channel, (controlmessage_types.ping_timeout,)) elif command_type == controlmessage_types.ping_timeout: self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, 'Ping timeout')) reconnecting = True elif command_type == controlmessage_types.reconnect: reconnecting = True else: error_message = 'Unknown control message: %s' % repr((command_type, *arguments)) self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, error_message)) else: assert False #unreachable if reconnecting: return True else: return False