python: Rewrite with CFFI

This commit is contained in:
Patrick Griffis 2017-09-02 17:52:25 -04:00
parent 6432694455
commit 706f9bca82
16 changed files with 1017 additions and 2850 deletions

View File

@ -2,7 +2,7 @@ sudo: required
services: docker
before_install:
- docker pull ubuntu:16.04
- docker run --privileged --cidfile=/tmp/cid ubuntu:16.04 /bin/sh -c 'apt-get update && apt-get install -y meson/xenial-backports libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libnotify-dev libpci-dev libperl-dev libproxy-dev libssl-dev python3-dev mono-devel desktop-file-utils'
- docker run --privileged --cidfile=/tmp/cid ubuntu:16.04 /bin/sh -c 'apt-get update && apt-get install -y meson/xenial-backports libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libnotify-dev libpci-dev libperl-dev libproxy-dev libssl-dev python3-dev python3-cffi mono-devel desktop-file-utils'
- docker commit `cat /tmp/cid` hexchat/ubuntu-ci
- rm -f /tmp/cid
install:

View File

@ -49,6 +49,10 @@ config_h.set('GLIB_VERSION_MIN_REQUIRED', 'GLIB_VERSION_2_34')
config_h.set('HAVE_MEMRCHR', cc.has_function('memrchr'))
config_h.set('HAVE_STRINGS_H', cc.has_header('strings.h'))
config_h.set_quoted('HEXCHATLIBDIR',
join_paths(get_option('prefix'), get_option('libdir'), 'hexchat/plugins')
)
if libssl_dep.found()
config_h.set('HAVE_X509_GET_SIGNATURE_NID',
cc.has_function('X509_get_signature_nid', dependencies: libssl_dep)

364
plugins/python/_hexchat.py Normal file
View File

@ -0,0 +1,364 @@
from contextlib import contextmanager
import inspect
import sys
from _hexchat_embedded import ffi, lib
__all__ = [
'EAT_ALL', 'EAT_HEXCHAT', 'EAT_NONE', 'EAT_PLUGIN', 'EAT_XCHAT',
'PRI_HIGH', 'PRI_HIGHEST', 'PRI_LOW', 'PRI_LOWEST', 'PRI_NORM',
'__doc__', '__version__', 'command', 'del_pluginpref', 'emit_print',
'find_context', 'get_context', 'get_info',
'get_list', 'get_lists', 'get_pluginpref', 'get_prefs', 'hook_command',
'hook_print', 'hook_print_attrs', 'hook_server', 'hook_server_attrs',
'hook_timer', 'hook_unload', 'list_pluginpref', 'nickcmp', 'prnt',
'set_pluginpref', 'strip', 'unhook',
]
__doc__ = 'HexChat Scripting Interface'
__version__ = (2, 0)
__license__ = 'GPL-2.0+'
EAT_NONE = 0
EAT_HEXCHAT = 1
EAT_XCHAT = EAT_HEXCHAT
EAT_PLUGIN = 2
EAT_ALL = EAT_HEXCHAT | EAT_PLUGIN
PRI_LOWEST = -128
PRI_LOW = -64
PRI_NORM = 0
PRI_HIGH = 64
PRI_HIGHEST = 127
# We need each module to be able to reference their parent plugin
# which is a bit tricky since they all share the exact same module.
# Simply navigating up to what module called it seems to actually
# be a fairly reliable and simple method of doing so if ugly.
def __get_current_plugin():
frame = inspect.stack()[1][0]
while '__plugin' not in frame.f_globals:
frame = frame.f_back
assert frame is not None
return frame.f_globals['__plugin']
# Keeping API compat
if sys.version_info[0] is 2:
def __decode(string):
return string
else:
def __decode(string):
return string.decode()
# ------------ API ------------
def prnt(string):
lib.hexchat_print(lib.ph, string.encode())
def emit_print(event_name, *args, **kwargs):
time = kwargs.pop('time', 0) # For py2 compat
cargs = []
for i in range(4):
arg = args[i].encode() if len(args) > i else b''
cstring = ffi.new('char[]', arg)
cargs.append(cstring)
if time is 0:
return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs)
else:
attrs = lib.hexchat_event_attrs_create(lib.ph)
attrs.server_time_utc = time
ret = lib.hexchat_emit_print(lib.ph, attrs, event_name.encode(), *cargs)
lib.hexchat_event_attrs_free(lib.ph, attrs)
return ret
def command(command):
lib.hexchat_command(lib.ph, command.encode())
def nickcmp(string1, string2):
return lib.hexchat_nickcmp(lib.ph, string1.encode(), string2.encode())
def strip(text, length=-1, flags=3):
stripped = lib.hexchat_strip(lib.ph, text.encode(), length, flags)
ret = __decode(ffi.string(stripped))
lib.hexchat_free(lib.ph, stripped)
return ret
def get_info(name):
ret = lib.hexchat_get_info(lib.ph, name.encode())
if ret == ffi.NULL:
return None
if name in ('gtkwin_ptr', 'win_ptr'):
# Surely there is a less dumb way?
ptr = repr(ret).rsplit(' ', 1)[1][:-1]
return ptr
return __decode(ffi.string(ret))
def get_prefs(name):
string_out = ffi.new('char**')
int_out = ffi.new('int*')
type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out)
if type is 0:
return None
elif type is 1:
return __decode(ffi.string(string_out[0]))
elif type is 2 or type is 3: # XXX: 3 should be a bool, but keeps API
return int_out[0]
else:
assert False
def __cstrarray_to_list(arr):
i = 0
ret = []
while arr[i] != ffi.NULL:
ret.append(ffi.string(arr[i]))
i += 1
return ret
__FIELD_CACHE = {}
def __get_fields(name):
return __FIELD_CACHE.setdefault(name,
__cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name)))
__FIELD_PROPERTY_CACHE = {}
def __cached_decoded_str(string):
return __FIELD_PROPERTY_CACHE.setdefault(string, __decode(string))
def get_lists():
return [__cached_decoded_str(field) for field in __get_fields(b'lists')]
class ListItem:
def __init__(self, name):
self._listname = name
def __repr__(self):
return '<{} list item at {}>'.format(self._listname, id(self))
def get_list(name):
# XXX: This function is extremely inefficient and could be interators and
# lazily loaded properties, but for API compat we stay slow
orig_name = name
name = name.encode()
if name not in __get_fields(b'lists'):
raise KeyError('list not available')
list_ = lib.hexchat_list_get(lib.ph, name)
if list_ == ffi.NULL:
return None
ret = []
fields = __get_fields(name)
def string_getter(field):
string = lib.hexchat_list_str(lib.ph, list_, field)
if string != ffi.NULL:
return __decode(ffi.string(string))
return ''
def ptr_getter(field):
if field == b'context':
ptr = lib.hexchat_list_str(lib.ph, list_, field)
ctx = ffi.cast('hexchat_context*', ptr)
return Context(ctx)
return None
getters = {
ord('s'): string_getter,
ord('i'): lambda field: lib.hexchat_list_int(lib.ph, list_, field),
ord('t'): lambda field: lib.hexchat_list_time(lib.ph, list_, field),
ord('p'): ptr_getter,
}
while lib.hexchat_list_next(lib.ph, list_) is 1:
item = ListItem(orig_name)
for field in fields:
getter = getters.get(ord(field[0]))
if getter is not None:
field_name = field[1:]
setattr(item, __cached_decoded_str(field_name), getter(field_name))
ret.append(item)
lib.hexchat_list_free(lib.ph, list_)
return ret
def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_command(lib.ph, command.encode(), priority, lib._on_command_hook,
help.encode() if help is not None else ffi.NULL,
hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_print(name, callback, userdata=None, priority=PRI_NORM):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook,
hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook,
hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_server(name, callback, userdata=None, priority=PRI_NORM):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook,
hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_server_attrs(name, callback, userdata=None, priority=PRI_NORM):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook,
hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_timer(timeout, callback, userdata=None):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata)
handle = lib.hexchat_hook_timer(lib.ph, timeout, lib._on_timer_hook, hook.handle)
hook.hexchat_hook = handle
return id(hook)
def hook_unload(callback, userdata=None):
plugin = __get_current_plugin()
hook = plugin.add_hook(callback, userdata, is_unload=True)
return id(hook)
def unhook(handle):
plugin = __get_current_plugin()
return plugin.remove_hook(handle)
def set_pluginpref(name, value):
if isinstance(value, str):
return bool(lib.hexchat_pluginpref_set_str(lib.ph, name.encode(), value.encode()))
elif isinstance(value, int):
return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value))
else:
# XXX: This should probably raise but this keeps API
return False
def get_pluginpref(name):
name = name.encode()
string_out = ffi.new('char[512]')
if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) is not 1:
return None
string = ffi.string(string_out)
# This API stores everything as a string so we have to figure out what
# its actual type was supposed to be.
if len(string) > 12: # Can't be a number
return __decode(string)
number = lib.hexchat_pluginpref_get_int(lib.ph, name)
if number == -1 and string != b'-1':
return __decode(string)
return number
def del_pluginpref(name):
return bool(lib.hexchat_pluginpref_delete(lib.ph, name.encode()))
def list_pluginpref():
prefs_str = ffi.new('char[4096]')
if lib.hexchat_pluginpref_list(lib.ph, prefs_str) is 1:
return __decode(prefs_str).split(',')
return []
class Context:
def __init__(self, ctx):
self._ctx = ctx
def __eq__(self, value):
if not isinstance(value, Context):
return False
return self._ctx == value._ctx
@contextmanager
def __change_context(self):
old_ctx = lib.hexchat_get_context(lib.ph)
if not self.set():
# XXX: Behavior change, previously used wrong context
lib.hexchat_print(lib.ph,
b'Context object refers to closed context, ignoring call')
return
yield
lib.hexchat_set_context(lib.ph, old_ctx)
def set(self):
# XXX: API addition, C plugin silently ignored failure
return bool(lib.hexchat_set_context(lib.ph, self._ctx))
def prnt(self, string):
with self.__change_context():
prnt(string)
def emit_print(self, event_name, *args, **kwargs):
time = kwargs.pop('time', 0) # For py2 compat
with self.__change_context():
return emit_print(event_name, *args, time=time)
def command(self, string):
with self.__change_context():
command(string)
def get_info(self, name):
with self.__change_context():
return get_info(name)
def get_list(self, name):
with self.__change_context():
return get_list(name)
def get_context():
ctx = lib.hexchat_get_context(lib.ph)
return Context(ctx)
def find_context(server=None, channel=None):
server = server.encode() if server is not None else ffi.NULL
channel = channel.encode() if channel is not None else ffi.NULL
ctx = lib.hexchat_find_context(lib.ph, server, channel)
if ctx == ffi.NULL:
return None
return Context(ctx)

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
import sys
import cffi
builder = cffi.FFI()
# hexchat-plugin.h
with open(sys.argv[1]) as f:
output = []
eat_until_endif = 0
# This is very specific to hexchat-plugin.h, it is not a cpp
for line in f:
if line.startswith('#define'):
continue
elif line.endswith('HEXCHAT_PLUGIN_H\n'):
continue
elif 'time.h' in line:
output.append('typedef int... time_t;')
elif line.startswith('#if'):
eat_until_endif += 1
elif line.startswith('#endif'):
eat_until_endif -= 1
elif eat_until_endif and '_hexchat_context' not in line:
continue
else:
output.append(line)
builder.cdef(''.join(output))
builder.embedding_api('''
extern "Python" int _on_py_command(char **, char **, void *);
extern "Python" int _on_load_command(char **, char **, void *);
extern "Python" int _on_unload_command(char **, char **, void *);
extern "Python" int _on_reload_command(char **, char **, void *);
extern "Python" int _on_say_command(char **, char **, void *);
extern "Python" int _on_command_hook(char **, char **, void *);
extern "Python" int _on_print_hook(char **, void *);
extern "Python" int _on_print_attrs_hook(char **, hexchat_event_attrs *, void *);
extern "Python" int _on_server_hook(char **, char **, void *);
extern "Python" int _on_server_attrs_hook(char **, char **, hexchat_event_attrs *, void *);
extern "Python" int _on_timer_hook(void *);
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
extern "Python" int _on_plugin_deinit(void);
static hexchat_plugin *ph;
''')
builder.set_source('_hexchat_embedded', '''
/* Python's header defines these.. */
#undef HAVE_MEMRCHR
#undef HAVE_STRINGS_H
#include "config.h"
#include "hexchat-plugin.h"
static hexchat_plugin *ph;
CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *);
CFFI_DLLEXPORT int _on_plugin_deinit(void);
int hexchat_plugin_init(hexchat_plugin *plugin_handle,
char **name_out, char **description_out,
char **version_out, char *arg)
{
if (ph != NULL)
{
puts ("Python plugin already loaded\\n");
return 0; /* Prevent loading twice */
}
ph = plugin_handle;
return _on_plugin_init(name_out, description_out, version_out, arg, HEXCHATLIBDIR);
}
int hexchat_plugin_deinit(void)
{
int ret = _on_plugin_deinit();
ph = NULL;
return ret;
}
''')
# python.py
with open(sys.argv[2]) as f:
builder.embedding_init_code(f.read())
# python.c
builder.emit_c_code(sys.argv[3])

View File

@ -0,0 +1 @@
from _hexchat import *

View File

@ -5,8 +5,18 @@ else
python_dep = dependency(python_opt, version: '>= 2.7')
endif
shared_module('python', 'python.c',
dependencies: [libgio_dep, hexchat_plugin_dep, python_dep],
python3_source = custom_target('python-bindings',
input: ['../../src/common/hexchat-plugin.h', 'python.py'],
output: 'python.c',
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
)
install_data(['_hexchat.py', 'hexchat.py', 'xchat.py'],
install_dir: join_paths(get_option('libdir'), 'hexchat/python')
)
shared_module('python', python3_source,
dependencies: [hexchat_plugin_dep, python_dep],
install: true,
install_dir: plugindir,
name_prefix: '',

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
EXPORTS
hexchat_plugin_init
hexchat_plugin_deinit
hexchat_plugin_get_info

497
plugins/python/python.py Normal file
View File

@ -0,0 +1,497 @@
from __future__ import print_function
import os
import sys
from contextlib import contextmanager
import importlib
import signal
import site
import traceback
import weakref
from _hexchat_embedded import ffi, lib
VERSION = b'2.0' # Sync with hexchat.__version__
PLUGIN_NAME = ffi.new('char[]', b'Python')
PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface'
% (sys.version_info[0], sys.version_info[1]))
PLUGIN_VERSION = ffi.new('char[]', VERSION)
hexchat = None
local_interp = None
hexchat_stdout = None
plugins = set()
@contextmanager
def redirected_stdout():
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
yield
sys.stdout = hexchat_stdout
sys.stderr = hexchat_stdout
if os.environ.get('HEXCHAT_LOG_PYTHON'):
def log(*args):
with redirected_stdout():
print(*args)
else:
def log(*args):
pass
class Stdout:
def __init__(self):
self.buffer = bytearray()
def write(self, string):
string = string.encode()
idx = string.rfind(b'\n')
if idx is not -1:
self.buffer += string[:idx]
lib.hexchat_print(lib.ph, bytes(self.buffer))
self.buffer = bytearray(string[idx + 1:])
else:
self.buffer += string
def isatty(self):
# FIXME: help() locks app despite this?
return False
class Attribute:
def __init__(self):
self.time = 0
def __repr__(self):
return '<Attribute object at {}>'.format(id(self))
class Hook:
def __init__(self, plugin, callback, userdata, is_unload):
self.is_unload = is_unload
self.plugin = weakref.proxy(plugin)
self.callback = callback
self.userdata = userdata
self.hexchat_hook = None
self.handle = ffi.new_handle(weakref.proxy(self))
def __del__(self):
log('Removing hook', id(self))
if self.is_unload is False:
assert self.hexchat_hook is not None
lib.hexchat_unhook(lib.ph, self.hexchat_hook)
if sys.version_info[0] is 2:
def compile_file(data, filename):
return compile(data, filename, 'exec', dont_inherit=True)
def compile_line(string):
try:
return compile(string, '<string>', 'eval', dont_inherit=True)
except SyntaxError:
# For some reason `print` is invalid for eval
# This will hide any return value though
return compile(string, '<string>', 'exec', dont_inherit=True)
else:
def compile_file(data, filename):
return compile(data, filename, 'exec', optimize=2, dont_inherit=True)
def compile_line(string):
return compile(string, '<string>', 'eval', optimize=2, dont_inherit=True)
class Plugin:
def __init__(self):
self.ph = None
self.name = ''
self.filename = ''
self.version = ''
self.description = ''
self.hooks = set()
self.globals = {
'__plugin': weakref.proxy(self),
'__name__': '__main__',
}
def add_hook(self, callback, userdata, is_unload=False):
hook = Hook(self, callback, userdata, is_unload=is_unload)
self.hooks.add(hook)
return hook
def remove_hook(self, hook):
for h in self.hooks:
if id(h) == hook:
ud = hook.userdata
self.hooks.remove(h)
return ud
else:
log('Hook not found')
def loadfile(self, filename):
try:
self.filename = filename
with open(filename) as f:
data = f.read()
compiled = compile_file(data, filename)
exec(compiled, self.globals)
try:
self.name = self.globals['__module_name__']
except KeyError:
lib.hexchat_print(lib.ph, b'Failed to load module: __module_name__ must be set')
return False
self.version = self.globals.get('__module_version__', '')
self.description = self.globals.get('__module_description__', '')
self.ph = lib.hexchat_plugingui_add(lib.ph, filename.encode(),
self.name.encode(),
self.description.encode(),
self.version.encode(),
ffi.NULL)
except Exception as e:
lib.hexchat_print(lib.ph, 'Failed to load module: {}'.format(e).encode())
traceback.print_exc()
return False
return True
def __del__(self):
log('unloading', self.filename)
for hook in self.hooks:
if hook.is_unload is True:
try:
hook.callback(hook.userdata)
except Exception as e:
log('Failed to run hook:', e)
traceback.print_exc()
del self.hooks
if self.ph is not None:
lib.hexchat_plugingui_remove(lib.ph, self.ph)
if sys.version_info[0] is 2:
def __decode(string):
return string
else:
def __decode(string):
return string.decode()
# There can be empty entries between non-empty ones so find the actual last value
def wordlist_len(words):
for i in range(31, 1, -1):
if ffi.string(words[i]):
return i
return 0
def create_wordlist(words):
size = wordlist_len(words)
return [__decode(ffi.string(words[i])) for i in range(1, size + 1)]
# This function only exists for compat reasons with the C plugin
# It turns the word list from print hooks into a word_eol list
# This makes no sense to do...
def create_wordeollist(words):
words = reversed(words)
last = None
accum = None
ret = []
for word in words:
if accum is None:
accum = word
elif word:
last = accum
accum = ' '.join((word, last))
ret.insert(0, accum)
return ret
def to_cb_ret(value):
if value is None:
return 0
else:
return int(value)
@ffi.def_extern()
def _on_command_hook(word, word_eol, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordlist(word_eol)
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
@ffi.def_extern()
def _on_print_hook(word, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordeollist(word)
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
@ffi.def_extern()
def _on_print_attrs_hook(word, attrs, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordeollist(word)
attr = Attribute()
attr.time = attrs.server_time_utc
return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr))
@ffi.def_extern()
def _on_server_hook(word, word_eol, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordlist(word_eol)
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
@ffi.def_extern()
def _on_server_attrs_hook(word, word_eol, attrs, userdata):
hook = ffi.from_handle(userdata)
word = create_wordlist(word)
word_eol = create_wordlist(word_eol)
attr = Attribute()
attr.time = attrs.server_time_utc
return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr))
@ffi.def_extern()
def _on_timer_hook(userdata):
hook = ffi.from_handle(userdata)
if hook.callback(hook.userdata) is True:
return 1
else:
hook.is_unload = True # Don't unhook
for h in hook.plugin.hooks:
if h == hook:
hook.plugin.hooks.remove(h)
break
return 0
@ffi.def_extern(error=3)
def _on_say_command(word, word_eol, userdata):
channel = ffi.string(lib.hexchat_get_info(lib.ph, b'channel'))
if channel == b'>>python<<':
python = ffi.string(word_eol[1])
lib.hexchat_print(lib.ph, b'>>> ' + python)
exec_in_interp(__decode(python))
return 0
def load_filename(filename):
filename = os.path.expanduser(filename)
if not os.path.isabs(filename):
configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir')))
filename = os.path.join(configdir, 'addons', filename)
if filename and not any(plugin.filename == filename for plugin in plugins):
plugin = Plugin()
if plugin.loadfile(filename):
plugins.add(plugin)
return True
return False
def unload_name(name):
if name:
for plugin in plugins:
if name in (plugin.name, plugin.filename,
os.path.basename(plugin.filename)):
plugins.remove(plugin)
return True
return False
def reload_name(name):
if name:
for plugin in plugins:
if name in (plugin.name, plugin.filename,
os.path.basename(plugin.filename)):
filename = plugin.filename
plugins.remove(plugin)
return load_filename(filename)
return False
@contextmanager
def change_cwd(path):
old_cwd = os.getcwd()
os.chdir(path)
yield
os.chdir(old_cwd)
def autoload():
configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir')))
addondir = os.path.join(configdir, 'addons')
try:
with change_cwd(addondir): # Maintaining old behavior
for f in os.listdir(addondir):
if f.endswith('.py'):
log('Autoloading', f)
# TODO: Set cwd
load_filename(os.path.join(addondir, f))
except FileNotFoundError as e:
log('Autoload failed', e)
def list_plugins():
if not plugins:
lib.hexchat_print(lib.ph, b'No python modules loaded')
return
lib.hexchat_print(lib.ph, b'Name Version Filename Description')
lib.hexchat_print(lib.ph, b'---- ------- -------- -----------')
for plugin in plugins:
basename = os.path.basename(plugin.filename).encode()
name = plugin.name.encode()
version = plugin.version.encode() if plugin.version else b'<none>'
description = plugin.description.encode() if plugin.description else b'<none>'
string = b'%-12s %-8s %-20s %-10s' %(name, version, basename, description)
lib.hexchat_print(lib.ph, string)
lib.hexchat_print(lib.ph, b'')
def exec_in_interp(python):
global local_interp
if not python:
return
if local_interp is None:
local_interp = Plugin()
local_interp.locals = {}
local_interp.globals['hexchat'] = hexchat
code = compile_line(python)
try:
ret = eval(code, local_interp.globals, local_interp.locals)
if ret is not None:
lib.hexchat_print(lib.ph, '{}'.format(ret).encode())
except Exception as e:
traceback.print_exc(file=hexchat_stdout)
@ffi.def_extern()
def _on_load_command(word, word_eol, userdata):
filename = ffi.string(word[2])
if filename.endswith(b'.py'):
load_filename(__decode(filename))
return 3
return 0
@ffi.def_extern()
def _on_unload_command(word, word_eol, userdata):
filename = ffi.string(word[2])
if filename.endswith(b'.py'):
unload_name(__decode(filename))
return 3
return 0
@ffi.def_extern()
def _on_reload_command(word, word_eol, userdata):
filename = ffi.string(word[2])
if filename.endswith(b'.py'):
reload_name(__decode(filename))
return 3
return 0
@ffi.def_extern(error=3)
def _on_py_command(word, word_eol, userdata):
subcmd = __decode(ffi.string(word[2])).lower()
if subcmd == 'exec':
python = __decode(ffi.string(word_eol[3]))
exec_in_interp(python)
elif subcmd == 'load':
filename = __decode(ffi.string(word[3]))
load_filename(filename)
elif subcmd == 'unload':
name = __decode(ffi.string(word[3]))
if not unload_name(name):
lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name')
elif subcmd == 'reload':
name = __decode(ffi.string(word[3]))
if not reload_name(name):
lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name')
elif subcmd == 'console':
lib.hexchat_command(lib.ph, b'QUERY >>python<<')
elif subcmd == 'list':
list_plugins()
elif subcmd == 'about':
lib.hexchat_print(lib.ph, b'HexChat Python interface version ' + VERSION)
else:
lib.hexchat_command(lib.ph, b'HELP PY')
return 3
@ffi.def_extern()
def _on_plugin_init(plugin_name, plugin_desc, plugin_version, arg, libdir):
global hexchat
global hexchat_stdout
signal.signal(signal.SIGINT, signal.SIG_DFL)
plugin_name[0] = PLUGIN_NAME
plugin_desc[0] = PLUGIN_DESC
plugin_version[0] = PLUGIN_VERSION
try:
libdir = __decode(ffi.string(libdir))
modpath = os.path.join(libdir, '..', 'python')
sys.path.append(os.path.abspath(modpath))
hexchat = importlib.import_module('hexchat')
except (UnicodeDecodeError, ImportError) as e:
lib.hexchat_print(lib.ph, b'Failed to import module: ' + repr(e).encode())
return 0
hexchat_stdout = Stdout()
sys.stdout = hexchat_stdout
sys.stderr = hexchat_stdout
lib.hexchat_hook_command(lib.ph, b'', 0, lib._on_say_command, ffi.NULL, ffi.NULL)
lib.hexchat_hook_command(lib.ph, b'LOAD', 0, lib._on_load_command, ffi.NULL, ffi.NULL)
lib.hexchat_hook_command(lib.ph, b'UNLOAD', 0, lib._on_unload_command, ffi.NULL, ffi.NULL)
lib.hexchat_hook_command(lib.ph, b'RELOAD', 0, lib._on_reload_command, ffi.NULL, ffi.NULL)
lib.hexchat_hook_command(lib.ph, b'PY', 0, lib._on_py_command, b'''Usage: /PY LOAD <filename>
UNLOAD <filename|name>
RELOAD <filename|name>
LIST
EXEC <command>
CONSOLE
ABOUT''', ffi.NULL)
lib.hexchat_print(lib.ph, b'Python interface loaded')
autoload()
return 1
@ffi.def_extern()
def _on_plugin_deinit():
global local_interp
global hexchat
global hexchat_stdout
global plugins
plugins = set()
local_interp = None
hexchat = None
hexchat_stdout = None
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
for mod in ('_hexchat', 'hexchat', 'xchat', '_hexchat_embedded'):
try:
del sys.modules[mod]
except KeyError:
pass
return 1

View File

@ -37,6 +37,9 @@
<AdditionalDependencies>"$(Python2Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python2Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
@ -48,12 +51,15 @@
<AdditionalDependencies>"$(Python2Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python2Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<None Include="python.def" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="python.c" />
<ClCompile Include="$(IntDir)python.c" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@ -37,6 +37,9 @@
<AdditionalDependencies>"$(Python3Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python3Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
@ -48,12 +51,20 @@
<AdditionalDependencies>"$(Python3Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python3Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<None Include="generate_plugin.py" />
<None Include="hexchat.py" />
<None Include="python.def" />
<None Include="python.py" />
<None Include="xchat.py" />
<None Include="_hexchat.py" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="python.c" />
<ClCompile Include="$(IntDir)python.c" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@ -9,13 +9,26 @@
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="python.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)python.c" />
</ItemGroup>
<ItemGroup>
<None Include="python.def">
<Filter>Resource Files</Filter>
</None>
<None Include="_hexchat.py">
<Filter>Source Files</Filter>
</None>
<None Include="generate_plugin.py">
<Filter>Source Files</Filter>
</None>
<None Include="hexchat.py">
<Filter>Source Files</Filter>
</None>
<None Include="python.py">
<Filter>Source Files</Filter>
</None>
<None Include="xchat.py">
<Filter>Source Files</Filter>
</None>
</ItemGroup>
</Project>

1
plugins/python/xchat.py Normal file
View File

@ -0,0 +1 @@
from _hexchat import *

View File

@ -93,10 +93,6 @@ endif
if get_option('with-plugin')
common_deps += libgmodule_dep
common_cflags += '-DHEXCHATLIBDIR="@0@"'.format(join_paths(get_option('prefix'),
get_option('libdir'),
'hexchat/plugins'))
install_headers('hexchat-plugin.h')
endif

View File

@ -64,6 +64,8 @@
<LuaShare Include="$(DepsRoot)\share\lua\**\*.lua" />
<LuaShare Include="$(DepsRoot)\share\lua\**\**\*.lua" />
<Typelib Include="$(DepsRoot)\lib\girepository-1.0\*.typelib" />
<None Include="$(Python3Path)\Lib\site-packages\_cffi_backend.*.pyd" />
<None Include="$(Python2Path)\Lib\site-packages\_cffi_backend.pyd" />
<Engines Include="$(DepsRoot)\lib\gtk-2.0\i686-pc-vs14\engines\**\*" />
@ -91,6 +93,9 @@
<Copy SourceFiles="@(LuaShare)" DestinationFiles="@(LuaShare->'$(HexChatRel)\share\lua\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(LuaLib)" DestinationFiles="@(LuaLib->'$(HexChatRel)\lib\lua\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(Typelib)" DestinationFiles="@(Typelib->'$(HexChatRel)\lib\girepository-1.0\%(Filename)%(Extension)')" />
<Copy SourceFiles="..\..\plugins\python\xchat.py" DestinationFolder="$(HexChatRel)\python" />
<Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(HexChatRel)\python" />
<Copy SourceFiles="..\..\plugins\python\_hexchat.py" DestinationFolder="$(HexChatRel)\python" />
<WriteLinesToFile File="$(HexChatRel)portable-mode" Lines="2" Overwrite="true" />

View File

@ -164,6 +164,7 @@ Source: "lib\girepository-1.0\*.typelib"; DestDir: "{app}\lib\girepository-1.0";
Source: "share\lua\*.lua"; DestDir: "{app}\share\lua"; Flags: ignoreversion; Components: langs\lua
Source: "share\lua\lgi\*.lua"; DestDir: "{app}\share\lua\lgi"; Flags: ignoreversion; Components: langs\lua
Source: "share\lua\lgi\override\*.lua"; DestDir: "{app}\share\lua\lgi\override"; Flags: ignoreversion; Components: langs\lua
Source: "plugins\hclua.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\lua
Source: "plugins\hcchecksum.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: plugins\checksum
Source: "plugins\hcexec.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: plugins\exec
@ -175,11 +176,15 @@ Source: "WinSparkle.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: pl
Source: "plugins\hcwinamp.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: plugins\winamp
Source: "share\system.png"; DestDir: "{app}\share"; Flags: ignoreversion; Components: plugins\sysinfo
Source: "plugins\hcsysinfo.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: plugins\sysinfo
Source: "plugins\hcperl.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\perl
Source: "python\*.py"; DestDir: "{app}\python"; Flags: ignoreversion; Components: langs\python
Source: "plugins\hcpython2.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\python\python2
Source: "_cffi_backend.pyd"; DestDir: "{app}"; Flags: ignoreversion; Components: langs\python\python2
Source: "plugins\hcpython3.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\python\python3
Source: "plugins\hcperl.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\perl
Source: "plugins\hclua.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\lua
Source: "_cffi_backend.cp3*.pyd"; DestDir: "{app}"; Flags: ignoreversion; Components: langs\python\python3
Source: "hexchat.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
Source: "hexchat-text.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: xctext