diff options
author | Jesusaves <cpntb1@ymail.com> | 2024-02-05 11:17:23 -0300 |
---|---|---|
committer | Jesusaves <cpntb1@ymail.com> | 2024-02-05 11:17:23 -0300 |
commit | 29ffe5de3c308013742b5bd97f7d75b09bd3b427 (patch) | |
tree | 7199cecaf204701770de171d007e561589b19762 /discord_rpc/connection | |
parent | f6b8c0c64757c73b6f2063d3a6d93ce2f8f527d5 (diff) | |
download | tkinter-29ffe5de3c308013742b5bd97f7d75b09bd3b427.tar.gz tkinter-29ffe5de3c308013742b5bd97f7d75b09bd3b427.tar.bz2 tkinter-29ffe5de3c308013742b5bd97f7d75b09bd3b427.tar.xz tkinter-29ffe5de3c308013742b5bd97f7d75b09bd3b427.zip |
Some button aligning, a CI template, and Discord RPC
Diffstat (limited to 'discord_rpc/connection')
-rw-r--r-- | discord_rpc/connection/__init__.py | 0 | ||||
-rw-r--r-- | discord_rpc/connection/ipc.py | 387 | ||||
-rw-r--r-- | discord_rpc/connection/rpc.py | 175 |
3 files changed, 562 insertions, 0 deletions
diff --git a/discord_rpc/connection/__init__.py b/discord_rpc/connection/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/discord_rpc/connection/__init__.py diff --git a/discord_rpc/connection/ipc.py b/discord_rpc/connection/ipc.py new file mode 100644 index 0000000..bbb26a7 --- /dev/null +++ b/discord_rpc/connection/ipc.py @@ -0,0 +1,387 @@ +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('<II', opcode, data_len) + # append header to data + data = header + data + # get new data size + data_len = len(data) + if self.__pipe is not None: + written = ctypes.c_ulong(0) + success = ctypes.windll.kernel32.WriteFile(self.__pipe, ctypes.c_char_p(data), data_len, + ctypes.byref(written), None) + if (not success) or (data_len != written.value): + self.log('warning', 'Failed to write data onto pipe.') + return False + return True + elif self.__sock is not None: + data_sent = 0 + while data_sent < data_len: + try: + sent = self.__sock.send(data[data_sent:], _msg_flags) + except Exception as e: + self.log('warning', e) + return False + if sent == 0: + self.log('warning', 'Socket connection broken!') + if data_sent == 0: + self.log('warning', 'No data sent; closing connection.') + self.close() + return False + data_sent += sent + return True + self.log('warning', 'write() executed code that shouldn\'t have run.') + return False + + def read(self): + ret_val = [False, None, None] + if not self.connected(): + self.log('warning', 'Cannot read if we haven\'t opened a connection!') + return ret_val + data = bytes() + header_size = struct.calcsize('<II') + # (is_successful_read, OpCode, data) + if self.__pipe is not None: + available = ctypes.c_ulong(0) + if not ctypes.windll.kernel32.PeekNamedPipe(self.__pipe, None, 0, None, ctypes.byref(available), None): + self.log('warning', 'Peek on pipe for header failed.') + self.close() + ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed'] + return ret_val + if available.value < header_size: + self.log('debug', 'Pipe doesn\'t have enough data to read in header.') + # assume this is like errno.EAGAIN + ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed'] + return ret_val + cb_read = ctypes.c_ulong(0) + buff = ctypes.create_string_buffer(header_size) + success = 0 + while not success: + success = ctypes.windll.kernel32.ReadFile(self.__pipe, buff, header_size, ctypes.byref(cb_read), None) + if success == 1: + # we successfully read the HEADER :O + # Note: we use RAW here, otherwise it'll be a 1 byte kinda weird thing + header = buff.raw + break + elif ctypes.windll.kernel32.GetLastError() != ERROR_MORE_DATA: + # we don't have more data; close pipe + self.log('warning', 'Failed to read in header from pipe.') + self.close() + ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed'] + return ret_val + opcode, data_len = struct.unpack('<II', header) + cb_read = ctypes.c_ulong(0) + buff = ctypes.create_string_buffer(data_len) + success = 0 + available = ctypes.c_ulong(0) + if not ctypes.windll.kernel32.PeekNamedPipe(self.__pipe, None, 0, None, ctypes.byref(available), None): + self.log('warning', 'Peek on pipe for data failed.') + self.close() + ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame'] + return ret_val + if available.value < data_len: + self.log('warning', 'Pipe doesn\'t have enough data to read in data.') + # assume this is like errno.EAGAIN + ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame'] + return ret_val + while not success: + success = ctypes.windll.kernel32.ReadFile(self.__pipe, buff, data_len, ctypes.byref(cb_read), None) + if success == 1: + # we successfully read the DATA :O + ret_val[0] = True + ret_val[1] = opcode + # value here actually works okay, so use that + # Note: raw also seems to work, but meh + data = buff.value + break + elif ctypes.windll.kernel32.GetLastError() != ERROR_MORE_DATA: + # we don't have more data; close pipe + self.log('warning', 'Failed to read in data from pipe.') + self.close() + ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame'] + return ret_val + elif self.__sock is not None: + packets = list() + while len(bytes().join(packets)) < header_size: + try: + packet = self.__sock.recv(header_size - len(bytes().join(packets))) + except Exception as e: + ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed'] + if hasattr(e, 'errno'): + if e.errno == errno.EAGAIN: + self.log('debug', e) + self.log('debug', 'errno == EAGAIN') + return ret_val + self.log('warning', 'Failed to read in header!') + self.log('warning', e) + self.close() + if packet is None or len(packet) == 0: + self.log('warning', 'Socket connection broken!') + if len(bytes().join(packets)) == 0: + self.log('warning', 'No data sent; closing connection.') + self.close() + ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed'] + return ret_val + packets.append(packet) + header = bytes().join(packets) + packets = list() + opcode, data_len = struct.unpack('<II', header) + self.log('debug', 'Opcode: {}, data length: {}'.format(opcode, data_len)) + while len(bytes().join(packets)) < data_len: + try: + packet = self.__sock.recv(data_len - len(bytes().join(packets))) + except Exception as e: + ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame'] + if hasattr(e, 'errno'): + if e.errno == errno.EAGAIN: + self.log('debug', e) + self.log('debug', 'errno == EAGAIN') + return ret_val + self.log('warning', 'Failed to read in data!') + self.log('warning', e) + if packet is None or len(packet) == 0: + self.log('warning', 'Socket connection broken!') + if len(bytes().join(packets)) == 0: + self.log('warning', 'No data sent; closing connection.') + self.close() + ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame'] + return ret_val + packets.append(packet) + data = bytes().join(packets) + ret_val[0] = True + ret_val[1] = opcode + if ret_val[0]: + if is_python3(): + data = to_unicode(data) + ret_val[2] = data + self.log('debug', 'Return values: {}'.format(ret_val)) + return ret_val + + def close(self): + # ensure we're using Windows before trying to close a pipe + # Note: This should **never** execute on a non-Windows machine! + if self.__pipe is not None and is_windows(): + ctypes.windll.kernel32.CloseHandle(self.__pipe) + self.__pipe = None + if self.__sock is not None: + try: + self.__sock.shutdown(socket.SHUT_RDWR) + self.__sock.close() + except Exception as e: + self.log('warning', e) + finally: + self.__sock = None + if self.__open: + self.__open = False + self.log('debug', 'Closed IPC connection.') + + def destroy(self): + # make sure we close everything + self.close() + # if we automatically set our own logger, clean it up + if self.__is_logging: + for handle in self.__logger.handlers[:]: + handle.close() + self.__logger.removeHandler(handle) + self.__logger = None + + @property + def is_open(self): + return self.__open + + def connected(self): + return self.is_open diff --git a/discord_rpc/connection/rpc.py b/discord_rpc/connection/rpc.py new file mode 100644 index 0000000..ad11007 --- /dev/null +++ b/discord_rpc/connection/rpc.py @@ -0,0 +1,175 @@ +from __future__ import absolute_import +import logging +import json +from ..codes import errorcodes +from ..codes import opcodes +from ..codes import statecodes +from ..util.utils import is_callable, json2dict, range +from .ipc import BaseConnection + + +_RPC_VERSION = 1 + + +class RpcConnection(object): + _connection = None + _state = statecodes.Disconnected + _app_id = None + _last_err_code = 0 + _last_err_msg = '' + _pipe_no = 0 + _on_connect = None + _on_disconnect = None + + def __init__(self, app_id, pipe_no=0, log=True, logger=None, log_file=None, log_level=logging.INFO): + self._connection = BaseConnection(log=log, logger=logger, log_file=log_file, log_level=log_level) + self._app_id = str(app_id) + if pipe_no in range(0, 10): + self._pipe_no = pipe_no + + def open(self): + if self.state == statecodes.Connected: + self.log('debug', 'Already connected; no need to open.') + return + + if self.state == statecodes.Disconnected: + self.connection.open(pipe_no=self._pipe_no) + if not self.connection.is_open: + self.log('warning', 'Failed to open IPC connection.') + return + + if self.state == statecodes.SentHandshake: + did_read, data = self.read() + if did_read: + cmd = data.get('cmd', None) + evt = data.get('evt', None) + if all(x is not None for x in (cmd, evt)) and cmd == 'DISPATCH' and evt == 'READY': + self.state = statecodes.Connected + if self.on_connect is not None: + self.on_connect(data) + self.log('info', 'IPC connected successfully.') + else: + data = {'v': _RPC_VERSION, 'client_id': self.app_id} + if self.connection.write(json.dumps(data), opcodes.Handshake): + self.state = statecodes.SentHandshake + self.log('debug', 'IPC connection sent handshake.') + else: + self.log('warning', 'IPC failed to send handshake.') + self.close() + + def close(self): + if self.on_disconnect is not None and self.state in (statecodes.Connected, statecodes.SentHandshake): + self.on_disconnect(self._last_err_code, self._last_err_msg) + self.log('debug', 'Attempting to close IPC connection.') + if self.connection is not None: + self.connection.close() + else: + self.log('warning', 'Called close without a connection!') + self.state = statecodes.Disconnected + + def write(self, data): + if isinstance(data, dict): + data = json.dumps(data) + if not self.connection.write(data, opcodes.Frame): + self.log('warning', 'Failed to write frame to IPC connection.') + self.close() + return False + return True + + def read(self): + if self.state not in (statecodes.Connected, statecodes.SentHandshake): + self.log('debug', 'We aren\'t connected, therefore we cannot read data yet.') + return False + while True: + did_read, opcode, data = self.connection.read() + self.log('debug', 'ipc.read(): read: {}, Opcode: {}, data: {}'.format(did_read, opcode, data)) + if not did_read: + err_reason = data[0] + if (err_reason == errorcodes.PipeClosed and not self.connection.is_open) \ + or err_reason == errorcodes.ReadCorrupt: + self._last_err_code = err_reason + self._last_err_msg = data[1] + self.log('debug', 'Failed to read; Connection closed. {}'.format(data)) + self.close() + return False, None + if opcode == opcodes.Close: + data = json2dict(data) + self._last_err_code = data.get('code', -1) + self._last_err_msg = data.get('message', '') + self.log('debug', 'Opcode == Close. Closing connection.') + self.close() + return False, None + elif opcode == opcodes.Frame: + data = json2dict(data) + self.log('debug', 'Successful read: {}'.format(data)) + return True, data + elif opcode == opcodes.Ping: + if not self.connection.write('', opcodes.Pong): + self.log('warning', 'Failed to send Pong message.') + self.close() + elif opcode == opcodes.Pong: + # Discord does nothing here + pass + else: + # something bad happened + self._last_err_code = errorcodes.ReadCorrupt + self._last_err_msg = 'Bad IPC frame.' + self.log('warning', 'Got a bad frame from IPC connection.') + self.close() + return False, None + + def destroy(self): + self.log('info', 'Destroying RPC connection.') + self.close() + self.connection.destroy() + self._connection = None + + def log(self, *args): + if self._connection is not None: + self._connection.log(*args) + + @property + def connection(self): + return self._connection + + @property + def state(self): + return self._state + + @state.setter + def state(self, state): + if isinstance(state, int) and state in (statecodes.Connected, statecodes.SentHandshake, + statecodes.Disconnected, statecodes.AwaitingResponse): + self._state = state + else: + self.log('warning', 'Invalid state number!') + + @property + def app_id(self): + return self._app_id + + @property + def is_open(self): + return self.state == statecodes.Connected + + @property + def on_connect(self): + return self._on_connect + + @on_connect.setter + def on_connect(self, callback): + if callback is None or is_callable(callback): + self._on_connect = callback + else: + self.log('warning', 'on_connect must be callable/None!') + + @property + def on_disconnect(self): + return self._on_disconnect + + @on_disconnect.setter + def on_disconnect(self, callback): + if callback is None or is_callable(callback): + self._on_disconnect = callback + else: + self.log('warning', 'on_disconnect must be callable/None!') |