summaryrefslogtreecommitdiff
path: root/discord_rpc/connection
diff options
context:
space:
mode:
authorJesusaves <cpntb1@ymail.com>2024-02-05 11:17:23 -0300
committerJesusaves <cpntb1@ymail.com>2024-02-05 11:17:23 -0300
commit29ffe5de3c308013742b5bd97f7d75b09bd3b427 (patch)
tree7199cecaf204701770de171d007e561589b19762 /discord_rpc/connection
parentf6b8c0c64757c73b6f2063d3a6d93ce2f8f527d5 (diff)
downloadtkinter-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__.py0
-rw-r--r--discord_rpc/connection/ipc.py387
-rw-r--r--discord_rpc/connection/rpc.py175
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!')