from __future__ import absolute_import import errno import logging from ..codes import errorcodes from ..util.utils import is_windows, range, get_temp_path, to_bytes, bytes, to_unicode, is_python3, is_callable import struct import sys if is_windows(): # we're going to have to do some ugly things, because Windows sucks import ctypes GENERIC_READ = 0x80000000 GENERIC_WRITE = 0x40000000 OPEN_EXISTING = 0x3 INVALID_HANDLE_VALUE = -1 PIPE_READMODE_MESSAGE = 0x2 ERROR_FILE_NOT_FOUND = 0x2 ERROR_PIPE_BUSY = 0xE7 ERROR_MORE_DATA = 0xEA BUFSIZE = 512 else: try: from socket import MSG_NOSIGNAL _msg_flags = MSG_NOSIGNAL except ImportError: _msg_flags = 0 try: from socket import SO_NOSIGPIPE _do_sock_opt = True except ImportError: _do_sock_opt = False import socket import fcntl from os import O_NONBLOCK class BaseConnection(object): """Generate IPC Connection handler.""" # *nix specific __sock = None # Windows specific __pipe = None __open = False __logger = None __is_logging = False def __init__(self, log=True, logger=None, log_file=None, log_level=logging.INFO): if not isinstance(log, bool): raise TypeError('log must be of bool type!') if log: if logger is not None: # Panda3D notifies are similar, so we simply check if we can make the same calls as logger if not hasattr(logger, 'debug'): raise TypeError('logger must be of type logging!') self.__logger = logger else: self.__logger = logging.getLogger(__name__) log_fmt = logging.Formatter('[%(asctime)s][%(levelname)s] ' + '%(name)s - %(message)s') if log_file is not None and hasattr(log_file, 'strip'): fhandle = logging.FileHandler(log_file) fhandle.setLevel(log_level) fhandle.setFormatter(log_fmt) self.__logger.addHandler(fhandle) shandle = logging.StreamHandler(sys.stdout) shandle.setLevel(log_level) shandle.setFormatter(log_fmt) self.__logger.addHandler(shandle) self.__is_logging = True def log(self, callback_name, *args): if self.__logger is not None: if hasattr(self.__logger, callback_name) and is_callable(self.__logger.__getattribute__(callback_name)): self.__logger.__getattribute__(callback_name)(*args) def __open_pipe(self, pipe_name, log_type='warning'): """ :param pipe_name: the named pipe string :param log_type: the log type to use (default 'warning') :return: opened(bool), try_again(bool) """ if not is_windows(): self.log('error', 'Attempted to call a Windows call on a non-Windows OS.') return pipe = ctypes.windll.kernel32.CreateFileW(pipe_name, GENERIC_READ | GENERIC_WRITE, 0, None, OPEN_EXISTING, 0, None) if pipe != INVALID_HANDLE_VALUE: self.__pipe = pipe return True, False err = ctypes.windll.kernel32.GetLastError() if err == ERROR_FILE_NOT_FOUND: self.log(log_type, 'File not found.') self.log(log_type, 'Pipe name: {}'.format(pipe_name)) return False, False elif err == ERROR_PIPE_BUSY: if ctypes.windll.kernel32.WaitNamedPipeW(pipe_name, 10000) == 0: self.log(log_type, 'Pipe busy.') return False, False else: # try again, should be free now self.log('debug', 'Pipe was busy, but should be free now. Try again.') return False, True # some other error we don't care about self.log('debug', 'Unknown error: {}'.format(err)) return False, False def open(self, pipe_no=None): if pipe_no is not None: if not isinstance(pipe_no, int): raise TypeError('pipe_no must be of type int!') if pipe_no not in range(0, 10): raise ValueError('pipe_no must be within range (0 <= pipe number < 10)!') if is_windows(): # NOTE: don't forget to use a number after ipc- pipe_name = u'\\\\.\\pipe\\discord-ipc-{}' if pipe_no is not None: # we only care about the first value if pipe_no isn't None opened, try_again = self.__open_pipe(pipe_name.format(pipe_no)) if opened: self.__open = True self.log('info', 'Connected to pipe {}, as user requested.'.format(pipe_no)) return elif try_again: self.open(pipe_no=pipe_no) return else: num = 0 while True: if num >= 10: break opened, try_again = self.__open_pipe(pipe_name.format(num), log_type='debug') if opened: self.__open = True self.log('debug', 'Automatically connected to pipe {}.'.format(num)) return if try_again: continue num += 1 # we failed to get a pipe self.__pipe = None self.log('warning', 'Could not open a connection.') else: self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) if self.__sock is None or self.__sock == -1: self.log('warning', 'Could not open socket.') self.close() return try: fcntl.fcntl(self.__sock, fcntl.F_SETFL, O_NONBLOCK) except Exception as e: self.log('warning', e) self.close() return if _do_sock_opt: try: socket.setsockopt(socket.SOL_SOCKET, SO_NOSIGPIPE) except Exception as e: self.log('warning', e) self.log('debug', 'Attempting to use sock as is. Notify a developer if an error occurs.') sock_addr = get_temp_path() if sock_addr.endswith('/'): sock_addr = sock_addr[:-1] sock_addr += '/discord-ipc-{}' if pipe_no is not None: ret_val = self.__sock.connect_ex(sock_addr.format(pipe_no)) if ret_val == 0: self.__open = True self.log('info', 'Connected to socket {}, as user requested.'.format(pipe_no)) return else: self.log('warning', 'Could not open socket {}.'.format(pipe_no)) self.close() else: for num in range(0, 10): ret_val = self.__sock.connect_ex(sock_addr.format(num)) if ret_val == 0: self.__open = True self.log('debug', 'Automatically connected to socket {}.'.format(num)) return self.log('warning', 'Could not open socket.') self.close() def write(self, data, opcode): if not self.connected(): self.log('warning', 'Cannot write if we aren\'t connected yet!') return False if not isinstance(opcode, int): raise TypeError('Opcode must be of int type!') if data is None: data = '' try: data = to_bytes(data) except Exception as e: self.log('warning', e) return False data_len = len(data) # the following data must be little endian unsigned ints # see: https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md#notes header = struct.pack('