/* * python.c -- Calling Python from epic. * * Copyright 2016 EPIC Software Labs. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notices, the above paragraph (the one permitting redistribution), * this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The names of the author(s) may not be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ /* Python commit #14 */ #include #include "irc.h" #include "ircaux.h" #include "array.h" #include "alias.h" #include "commands.h" #include "functions.h" #include "output.h" #include "ifcmd.h" #include "extlang.h" #include "newio.h" #include "server.h" void output_traceback (void); static int p_initialized = 0; static PyObject *global_vars = NULL; /* * ObRant * * So Python is a language like ircII where there is a distinction made between * "statements" and "expressions". In both languages, a "statement" is a call * that does not result in a return value, and expression is. I can't blame * python for this. But it is different from Perl, and Ruby, which treat * everything as a "callable" and then you either get a return value (for an * expression) or an empty string (for a statement). * * In order to make python support work, though, we have to honor this * distinction. I've chosen to do this through the /PYTHON command and the * $python() function * * You can only use the /PYTHON command to run statements. * Using /PYTHON with an expression results in an exception being thrown. * * You can only use the $python() function to evaluate expressions * Using $python() with a statement results in an exception being thrown. * * How do you know whether what you're doing is an expression or a statement, * when if you just throw everything into one file you don't have to worry * about it? Good question. I don't know. Good luck! */ /* * I owe many thanks to * https://docs.python.org/3.6/extending/embedding.html * https://docs.python.org/3.6/c-api/veryhigh.html * https://docs.python.org/3/c-api/init.html * https://docs.python.org/3/c-api/exceptions.html * https://www6.software.ibm.com/developerworks/education/l-pythonscript/l-pythonscript-ltr.pdf * http://boost.cppll.jp/HEAD/libs/python/doc/tutorial/doc/using_the_interpreter.html * (for explaining what the "start" flag is to PyRun_String) * for teaching how to embed and extend Python in a C program. */ /* * I owe many many thanks to skully for working with me on this * and giving me good advice on how to make this not suck. */ /*************************************************************************/ /* * Extended Python by creating an "import epic" module. */ /* * Psuedo-code of how to create a python module that points at your C funcs * * These functions provide the basic facilities of ircII. * All of these functions take one string argument. * epic.echo - yell() -- Like /echo, unconditionally output to screen * epic.say - say() -- Like /xecho -s, output if not suppressed with ^ * epic.cmd - runcmds() -- Run a block of code, * but don't expand $'s (like from the input line) * epic.eval - runcmds() -- Run a block of code, * expand $'s, but $* is [] (this is lame) * epic.expr - parse_inline() -- Evaluate an expression string * and return the result * epic.call - call_function() -- Evaluate a "funcname(argument list)" * string and return the result * * These functions provide a route-around of ircII, if that's what you want. * All of these functions take a symbol name ("name") and a string containing * the arguments (if appropriate) * NONE OF THESE FUNCTIONS UNDERGO $-EXPANSION! * (If you need that, use the stuff ^^^ above) * * epic.run_command - run an alias (preferentially) or a builtin command. * Example: epic.run_command("xecho", "-w 0 hi there! This does not get expanded") * epic.call_function - call an alias (preferentially) or a builtin function * Example: epic.call_function("windowctl", "refnums") * epic.get_set - get a /SET value (only) * Example: epic.get_set("mail") * epic.get_assign - get an /ASSIGN value (only) * Example: epic.get_assign("myvar") * epic.get_var - get an /ASSIGN value (preferentially) or a /SET * epic.set_set - set a /SET value (only) * Example: epic.set_set("mail", "ON") * epic.set_assign - set a /ASSIGN value * Example: epic.set_assign("myvar", "5") * * These functions allow you to register file descriptors (like socket) * with epic, which will call back your method when they're interesting. * * epic.callback_when_readable(python_file, python_function, python_object) * epic.callback_when_writable(python_file, python_function, python_object) * When 'python_function' is None, it cancels the callback. * The 'python_function' needs to be a static function (?) * The 'python_object' can be anything, i guess. */ /********************** Higher level interface to things *********************/ /* * epic.echo("hello, world") -- Output something without a banner (like /echo) * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string containing stuff to output * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * 0 - The stuff was displayed successfully. */ static PyObject * epic_echo (PyObject *self, PyObject *args) { char * str; /* * https://docs.python.org/3/extending/extending.html * tells me that if PyArg_ParseTuple() doesn't like the * argument list, it will set an exception and return * NULL. And all i should do is return NULL. * So that is why i do that here (and everywhere) */ if (!PyArg_ParseTuple(args, "z", &str)) { return NULL; } yell("%s", str); return PyLong_FromLong(0L); } /* * epic.say("Warning, WIll Robinson") -- Output something with a banner * (like internal commands do) * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string containing stuff to output * * Notes: * The banner is (of course) /SET BANNER. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple (and threw exception) * 0 - The stuff was displayed successfully. */ static PyObject * epic_say (PyObject *self, PyObject *args) { char * str; if (!PyArg_ParseTuple(args, "z", &str)) { return NULL; } say("%s", str); return PyLong_FromLong(0L); } /* * epic.cmd("join #epic") -- Run an ircII command like at the input line * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string containing an ircII command statement * * Notes: * 'args' is run as a single ircII statement (as if you had typed * it at the command line). $'s are not expanded; semicolons are * not treated as statement separates, and braces have no special * meaning. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple (and threw exception) * 0 - The stuff was run successfully. */ static PyObject * epic_cmd (PyObject *self, PyObject *args) { char * str; if (!PyArg_ParseTuple(args, "z", &str)) { return NULL; } runcmds("$*", str); return PyLong_FromLong(0L); } /* * epic.eval("echo My nick is $N") -- Run an ircII command line in an alias * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string containing an ircII statement * * Notes: * $* is treated as the empty string ([]) and so any $0, $1 type * expansions will expand to the empty string. Maybe this will * change in the future, and you'll be permitted to pass in $*. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple (and threw exception) * 0 - The stuff ran successfully. */ static PyObject * epic_eval (PyObject *self, PyObject *args) { char * str; if (!PyArg_ParseTuple(args, "z", &str)) { return NULL; } runcmds(str, ""); return PyLong_FromLong(0L); } /* * epic.expr("var + 2 * numonchannel()") -- Return the result of an expression * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string containing an ircII expression * * Notes: * $* is treated as the empty string ([]) and so any $0, $1 type * expansions will expand to the empty string. Maybe this will * change in the future, and you'll be permitted to pass in $*. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple (and threw exception) * NULL - Py_BuildValue() threw an exception converting the expr * result (a string) into a python string. * A string - The result of the expression * - All ircII expressions result in a string, even if that * string contains an integer or whatever. * ie, "2 + 2" is "4" == a string containing the number 4. */ static PyObject * epic_expr (PyObject *self, PyObject *args) { char * str; char * exprval; PyObject *retval; if (!PyArg_ParseTuple(args, "z", &str)) { return NULL; } exprval = parse_inline(str, ""); if (!(retval = Py_BuildValue("z", exprval))) { return NULL; } new_free(&exprval); return retval; } /* * epic.expand("text with ${2+2} $vars in it") -- Expand a string and return it * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string containing an ircII string * * Notes: * $* is treated as the empty string ([]) and so any $0, $1 type * expansions will expand to the empty string. Maybe this will * change in the future, and you'll be permitted to pass in $*. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple (and threw exception) * NULL - Py_BuildValue() couldn't convert the retval to python string (throws exception) * A string - The result of the expansion */ static PyObject * epic_expand (PyObject *self, PyObject *args) { char * str; char * expanded; PyObject *retval; if (!PyArg_ParseTuple(args, "z", &str)) { return NULL; } expanded = expand_alias(str, ""); retval = Py_BuildValue("z", expanded); new_free(&expanded); return retval; } /* * epic.call("function(arglist)") -- Call a function directly (with $-expansion) * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string containing a function call, with parenthesis * * Notes: * The 'arglist' will be expanded ($-expandos will be honored) * $* will be treated as the empty string (see the caveats above) * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple (and threw exception) * NULL - Py_BuildValue() couldn't convert the retval to python string * (throws exception) * A string - The return value of the function call * All function calls return exactly one string, even if * that string contains a number or a list of words. * You may need to process the string in python. */ static PyObject * epic_call (PyObject *self, PyObject *args) { char * str; char * funcval; PyObject *retval; if (!PyArg_ParseTuple(args, "z", &str)) { return NULL; } funcval = call_function(str, ""); retval = Py_BuildValue("z", funcval); new_free(&funcval); return retval; } /* Lower level interfaces to things */ /* * epic.run_command("COMMAND", "arglist") - Call a command (alias or builtin) * directly WITHOUT expansion * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing: * 1. A string containing an alias name or a built in command name * 2. A string representing the argument list * * Note: All ircII commands take one string as the argument, even if that string * contains some serialization of a collection. Technically each command * is permitted to do whatever it wants with its arguments; but the * convention is to accept a space separated list of words. Again, this * is not a requirement, justthe way most things work. * * Note: The argument list is _NOT_ expanded; it is passed literally in to * the cmd. * Note: XXX - The $* that is passed in is NULL; I'm not sure if this is * correct or not. * Note: Aliases are prefered to builtin commands, just like ircII does it. * * Return Value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * NULL / NameError - The command you tried to run doesn't exist. * None - The command was run successfully */ static PyObject * epic_run_command (PyObject *self, PyObject *args) { char * symbol; char * my_args; void (*builtin) (const char *, char *, const char *) = NULL; const char * alias = NULL; void * arglist = NULL; if (!PyArg_ParseTuple(args, "zz", &symbol, &my_args)) { return NULL; } upper(symbol); alias = get_cmd_alias(symbol, &arglist, &builtin); if (alias) call_user_command(symbol, alias, my_args, arglist); else if (builtin) builtin(symbol, my_args, empty_string); else { PyErr_Format(PyExc_NameError, "%s : Command does not exist", symbol); return NULL; } /* Success! */ Py_INCREF(Py_None); return Py_None; } /* * epic.call_function("FUNCTION", "arglist") -- Call a function * (alias or builtin) directly WITHOUT expansion * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing: * 1. A string containing an alias name or built in function name * 2. A string representing the argument list * * Note: All ircII functions take one string as their argument, even if that * string contains some representation of a collection. Technically each * function is permitted to do whatever it wants with its arguments; but * the convention is to accept a space separated list of words. Again, * this is not a requirement, just the way most things work. * * Note: The argument list is _NOT_ expanded; it is passed literally to the fn. * Note: XXX - The $* that is passed in is NULL; * I'm not sure if this is correct or not. * Note: Aliases are preferred to builtins, just like ircII. * * Return Value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * NULL / NameError - The function you tried to run doesn't exist. * A string - the return value of the function (which may be zero-length for its own reasons) */ static PyObject * epic_call_function (PyObject *self, PyObject *args) { char * symbol; char * my_args; PyObject *retval; const char *alias; void * arglist; char * (*builtin) (char *) = NULL; char * funcval = NULL; if (!PyArg_ParseTuple(args, "zz", &symbol, &my_args)) { return PyLong_FromLong(-1); } upper(symbol); alias = get_func_alias(symbol, &arglist, &builtin); if (alias) funcval = call_user_function(symbol, alias, my_args, arglist); else if (builtin) funcval = builtin(my_args); else { PyErr_Format(PyExc_NameError, "%s : Function does not exist", symbol); return NULL; } retval = Py_BuildValue("z", funcval); new_free(&funcval); return retval; } /* * epic.get_set("SETNAME") - Return the value of /SET SETNAME * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing: * 1. A /SET name (ie, "AUTO_REJOIN") * * Note: Although /SETs have types (integer, boolean, string), the * value of a SET is always a string. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * NULL / NameError - The set you tried to fetch doesn't exist. * A string - the value of /SET SETNAME */ static PyObject * epic_get_set (PyObject *self, PyObject *args) { char * symbol; PyObject *retval; char * funcval = NULL; if (!PyArg_ParseTuple(args, "z", &symbol)) { return NULL; } upper(symbol); if (make_string_var2(symbol, &funcval) < 0) { PyErr_Format(PyExc_NameError, "%s : SET variable does not exist", symbol); return NULL; } retval = Py_BuildValue("z", funcval); new_free(&funcval); return retval; } /* * epic.get_assign("VARNAME") - Return the value of /ASSIGN VARNAME * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing: * 1. A /ASSIGN name (ie, "MYVAR") * * Note: Variables are always strings, even if they contain a number. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * NULL / NameError - The set you tried to fetch doesn't exist. * A string - the value of /ASSIGN VARNAME (or empty if it does not exist) * XXX - This is probably wrong, asking for an unknown * assign should probably return None or throw an * exception or something. */ static PyObject * epic_get_assign (PyObject *self, PyObject *args) { char * symbol; PyObject *retval; char * (*efunc) (void) = NULL; IrcVariable * sfunc = NULL; const char * assign = NULL; if (!PyArg_ParseTuple(args, "z", &symbol)) { return NULL; } upper(symbol); if (!(assign = get_var_alias(symbol, &efunc, &sfunc))) { PyErr_Format(PyExc_NameError, "%s : ASSIGN variable does not exist", symbol); return NULL; } retval = Py_BuildValue("z", assign); /* _DO NOT_ FREE 'assign'! */ return retval; } /* * epic.get_var("NAME") - Return the value of a variable [see notes] * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing: * 1. A variable symbol name of some sort * * Note: A variable name can be a local variable, a global variable, or a set * variable. The return value is what you would get if you did $. * Note: XXX - Because of how this is implemented, this supports ":local" and * "::global", although I'm not sure it will stay this way forever. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * A string - The value of $ * The zero-length string if $ does not exist. * * Note: Because of how ircII does this, it is not possible to differentiate * between a variable that does not exist and a variable that is unset. * If you need that level of granularity (which should be unusual) * you should use epic.get_var(), epic.get_assign(), etc. */ static PyObject * epic_get_var (PyObject *self, PyObject *args) { char * symbol; PyObject *retval; char * funcval = NULL; if (!PyArg_ParseTuple(args, "z", &symbol)) { return NULL; } upper(symbol); funcval = get_variable(symbol); retval = Py_BuildValue("z", funcval); new_free(&funcval); return retval; } /* * epic.set_set("setname", "value") -- do a /SET * XXX - TODO - XXX * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string - the SET to be set * 2. A string - the value to be SET * Note: * SETs have types, which are not currently exposed through this * interface. You need to pass in a string that contains a value * of the correct type (ie, if the SET is an integer, you need to * pass in a string containing an integer. If the SET is a boolean, * you need to pass in a string containing "TRUE" or "FALSE".) * Who knows -- this may improve in the future. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * NULL / NotImplelmentedError - This function is not implemented yet */ static PyObject * epic_set_set (PyObject *self, PyObject *args) { char * symbol; char * value; if (!PyArg_ParseTuple(args, "zz", &symbol, &value)) { return NULL; } /* XXX - TODO - /SET symbol value */ PyErr_Format(PyExc_NotImplementedError, "Not Implemented Yet"); return NULL; return PyLong_FromLong(0); } /* * epic.set_assign("varname", "value") -- do an /ASSIGN * XXX - TODO - XXX * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. A string -- the variable to be /ASSIGNed * 2. A string -- the value to be /ASSIGNED * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * NULL / NotImplelmentedError - This function is not implemented yet */ static PyObject * epic_set_assign (PyObject *self, PyObject *args) { char * symbol; char * value; if (!PyArg_ParseTuple(args, "zz", &symbol, &value)) { return NULL; } /* XXX - TODO - /ASSIGN symbol value */ PyErr_Format(PyExc_NotImplementedError, "Not Implemented Yet"); return NULL; } /* * * * * * * * * */ typedef struct { int vfd; PyObject * read_callback; PyObject * write_callback; PyObject * except_callback; int flags; } PythonFDCallback; /* This should be in a header file somewhere... */ #if defined(HAVE_SYSCONF) && defined(_SC_OPEN_MAX) # define IO_ARRAYLEN sysconf(_SC_OPEN_MAX) #else # if defined(FD_SETSIZE) # define IO_ARRAYLEN FD_SETSIZE # else # define IO_ARRAYLEN NFDBITS # endif #endif PythonFDCallback **python_vfd_callback = NULL; void init_python_fd_callbacks (void) { int vfd; int max_fd = IO_ARRAYLEN; if (python_vfd_callback) return; /* Panic is so 12008 */ python_vfd_callback = (PythonFDCallback **)new_malloc(sizeof(PythonFDCallback *) * max_fd); for (vfd = 0; vfd < max_fd; vfd++) python_vfd_callback[vfd] = NULL; } PythonFDCallback * get_python_vfd_callback (int vfd) { if (vfd < 0 || vfd > IO_ARRAYLEN) return NULL; return python_vfd_callback[vfd]; } int set_python_vfd_callback (int vfd, PythonFDCallback *callback) { if (callback == NULL) return -1; if (python_vfd_callback[vfd] && python_vfd_callback[vfd] != callback) return -1; if (callback->vfd == -1) callback->vfd = vfd; if (vfd != callback->vfd) return -1; python_vfd_callback[vfd] = callback; return 0; } PythonFDCallback * new_python_fd_callback (int vfd) { PythonFDCallback *callback; callback= (PythonFDCallback *)new_malloc(sizeof(PythonFDCallback)); callback->vfd = vfd; callback->read_callback = NULL; callback->write_callback = NULL; callback->except_callback = NULL; callback->flags = 0; set_python_vfd_callback(vfd, callback); return callback; } int destroy_python_fd_callback (int vfd) { PythonFDCallback *callback; if (!(callback = python_vfd_callback[vfd])) return -1; callback->vfd = -1; callback->read_callback = NULL; callback->write_callback = NULL; callback->except_callback = NULL; callback->flags = 0; python_vfd_callback[vfd] = NULL; new_free((void **)&callback); return 0; } /* * call_python_function_1arg - Call a python function to handle IO callbacks * * Python IO callbacks take one argument - the file descriptor. * Python IO callbacks _may not throw exceptions_. * Exceptions are considered a "broken" situation and will result in * a call to the except_callback to repair the situation. * If the except_callback fails, then it's game over for this fd. * * Retval * -1 - The callback failed (threw an exception) * 0 - The callback did not fail */ int call_python_function_1arg (PyObject *pFunc, int vfd) { PyObject *args_py = NULL; PyObject *pArgs = NULL, *pRetVal = NULL; /* PyObject *retval_repr = NULL; */ if (!PyCallable_Check(pFunc)) { my_error("python_fd_callback: The callback was not a function"); goto c_p_f_error; } if (!(pArgs = PyTuple_New(1))) goto c_p_f_error; if (!(args_py = Py_BuildValue("i", vfd))) goto c_p_f_error; if ((PyTuple_SetItem(pArgs, 0, args_py))) goto c_p_f_error; args_py = NULL; /* args_py now belongs to the tuple! */ if (!(pRetVal = PyObject_CallObject(pFunc, pArgs))) goto c_p_f_error; if (!(/*retval_repr = */PyObject_Repr(pRetVal))) goto c_p_f_error; goto c_p_f_cleanup; c_p_f_error: output_traceback(); return -1; c_p_f_cleanup: Py_XDECREF(pArgs); Py_XDECREF(pRetVal); return 0; } void do_python_fd (int vfd) { PythonFDCallback *callback; char buffer[BIG_BUFFER_SIZE]; int n; if (!((callback = get_python_vfd_callback(vfd)))) { yell("do_python_fd: FD %d doesn't belong to me - new_close()ing it.", vfd); new_close(vfd); return; } if ((n = dgets(vfd, buffer, BIG_BUFFER_SIZE, -1)) > 0) { if (callback->read_callback) { if ((call_python_function_1arg(callback->read_callback, vfd))) { if ((call_python_function_1arg(callback->except_callback, vfd))) { yell("do_python_fd: FD %d failed: both ordinary and error callback failed.", vfd); new_close(vfd); } } } else if (callback->write_callback) { if ((call_python_function_1arg(callback->write_callback, vfd))) { if ((call_python_function_1arg(callback->except_callback, vfd))) { yell("do_python_fd: FD %d failed: both ordinary and error callback failed.", vfd); new_close(vfd); } } } } else if (n < 0) { if (callback->except_callback) { if ((call_python_function_1arg(callback->except_callback, vfd))) { yell("do_python_fd: FD %d failed: both ordinary and error callback failed.", vfd); new_close(vfd); } } } } void do_python_fd_failure (int vfd, int error) { PythonFDCallback *callback; if (!((callback = get_python_vfd_callback(vfd)))) { yell("do_python_fd_failure: FD %d doesn't belong to me - new_close()ing it myself.", vfd); new_close(vfd); return; } if (callback->except_callback) call_python_function_1arg(callback->except_callback, vfd); } /* * epic.callback_when_readable(fd, read_callback, except_function, flags) * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. fd - An integer - a unix file descriptor (from python) * 2. read_callback - A CallableObject (ie, a method) - A python * method that takes one integer argument. It will * be called each time the 'fd' is readable. * You must "handle" the fd, or it will busy-loop. * If you block while handling the fd, epic * will block. * Arg1 - fd * 3. except_callback - A CallableObject (ie, a method) - A python * method that takes two integer arguments. It will * be called if the 'fd' becomes invalid. This * would happen if somneone close()d the FD, but * didn't call _epic.cancel_callback(). * You should call _epic.cancel_callback(), or * it will busy-loop. If you block while handling * the fd, epic will block. * Arg1 - fd, Arg2 - error code * 4. flags - Reserved for future expansion. Pass in 0 for now. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * 0 - The command succeeded */ static PyObject * epic_callback_when_readable (PyObject *self, PyObject *args) { long vfd, flags; PyObject *read_callback, *except_callback; PythonFDCallback *callback; /* * https://docs.python.org/3/extending/extending.html * tells me that if PyArg_ParseTuple() doesn't like the * argument list, it will set an exception and return * NULL. And all i should do is return NULL. * So that is why i do that here (and everywhere) */ if (!PyArg_ParseTuple(args, "lOOl", &vfd, &read_callback, &except_callback, &flags)) { return NULL; } /* * 1. Look up python fd record, create if necessary * 2. Set read_callback in the python fd record * 3. Call new_open() if necessary */ if (!(callback = get_python_vfd_callback(vfd))) callback = new_python_fd_callback(vfd); else { if (callback->write_callback) new_close_with_option(vfd, 1); } callback->write_callback = NULL; callback->read_callback = read_callback; callback->except_callback = except_callback; callback->flags = flags; new_open(vfd, do_python_fd, NEWIO_PASSTHROUGH_READ, 0, from_server); new_open_failure_callback(vfd, do_python_fd_failure); return PyLong_FromLong(0L); } /* * epic.callback_when_writable(fd, write_callback, except_function, flags) * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. fd - An integer - a unix file descriptor (from python) * 2. read_callback - A CallableObject (ie, a method) - A python * method that takes one integer argument. It will * be called each time the 'fd' is writable. * You must "handle" the fd, or it will busy-loop. * If you block while handling the fd, epic * will block. * 3. except_callback - A CallableObject (ie, a method) - A python * method that takes one integer argument. It will * be called if the 'fd' becomes invalid. This * would happen if somneone close()d the FD, but * didn't call _epic.cancel_callback(). * You should call _epic.cancel_callbacks(), or * it will busy-loop. If you block while handling * the fd, epic will block. * 4. flags - Reserved for future expansion. Pass in 0 for now. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * 0 - The command succeeded */ static PyObject * epic_callback_when_writable (PyObject *self, PyObject *args) { long vfd, flags; PyObject *write_callback, *except_callback; PythonFDCallback *callback; /* * https://docs.python.org/3/extending/extending.html * tells me that if PyArg_ParseTuple() doesn't like the * argument list, it will set an exception and return * NULL. And all i should do is return NULL. * So that is why i do that here (and everywhere) */ if (!PyArg_ParseTuple(args, "lOOl", &vfd, &write_callback, &except_callback, &flags)) { return NULL; } /* * 1. Look up python fd record, create if necessary * 2. Set write_callback in the python fd record * 3. Call new_open() if necessary. */ if (!(callback = get_python_vfd_callback(vfd))) callback = new_python_fd_callback(vfd); else { if (callback->read_callback) new_close_with_option(vfd, 1); } callback->read_callback = NULL; callback->write_callback = write_callback; callback->except_callback = except_callback; callback->flags = flags; new_open(vfd, do_python_fd, NEWIO_PASSTHROUGH_WRITE, 0, from_server); new_open_failure_callback(vfd, do_python_fd_failure); return PyLong_FromLong(0L); } /* * epic.cancel_callback(fd) * * Arguments: * self - ignored (the "epic" module) * args - A tuple containing * 1. fd - An integer - a unix file descriptor (from python) * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * 0 - The command succeeded */ static PyObject * epic_cancel_callback (PyObject *self, PyObject *args) { long vfd; PythonFDCallback *callback; /* * https://docs.python.org/3/extending/extending.html * tells me that if PyArg_ParseTuple() doesn't like the * argument list, it will set an exception and return * NULL. And all i should do is return NULL. * So that is why i do that here (and everywhere) */ if (!PyArg_ParseTuple(args, "l", &vfd)) { return NULL; } /* * 1. Look up python fd record (bail if does not exist) * 2. Call new_close_with_option() * 3. Clear the python fd record * (4. The python script has to close the fd...) */ if ((callback = get_python_vfd_callback(vfd))) { callback->read_callback = NULL; callback->write_callback = NULL; callback->except_callback = NULL; callback->flags = 0; new_close_with_option(vfd, 1); } return PyLong_FromLong(0L); } /* * INTERNAL USE ONLY -- * When you want to register a python module.method as a hardcoded * builtin ircII command, you need a BUILT_IN_COMMAND() to be registered * as the callback. Because of how ircII works, the /COMMAND that you * run ends up in 'command', so this shim just does the glue: * * In python: * epic.builtin_cmd("module.method") * In ircII: * /MODULE.METHOD ...args... * * XXX - It's not clear if 'subargs' should be passed to the python function. */ BUILT_IN_COMMAND(pyshim) { char * retval = NULL; retval = call_python_directly(command, args); new_free(&retval); } /* * epic.builtin_cmd("module.method") -- Register a python module.method * as a builtin ircII cmd * Arguments: * self - ignored (the "epic" object) * args - A tuple containing * 1. A string - the name of "module.method") * This will become /MODULE.METHOD in ircII. * * Return value: * NULL - PyArg_ParseTuple() didn't like your tuple * (and threw exception) * None - The command was registered successfully */ static PyObject * epic_builtin_cmd (PyObject *self, PyObject *args) { char * symbol; if (!PyArg_ParseTuple(args, "z", &symbol)) { return NULL; } /* XXX TODO - Test this -- does 'symbol' need to be strdup()d? XXX TODO */ add_builtin_cmd_alias(symbol, pyshim); /* Success! */ Py_INCREF(Py_None); return Py_None; } static PyMethodDef epicMethods[] = { /* Higher level facilities - $-expansion supported */ { "echo", epic_echo, METH_VARARGS, "Unconditionally output to screen (yell)" }, { "say", epic_say, METH_VARARGS, "Output to screen unless suppressed (say)" }, { "cmd", epic_cmd, METH_VARARGS, "Run a block statement without expansion (runcmds)" }, { "eval", epic_eval, METH_VARARGS, "Run a block statement with expansion (but $* is empty)" }, { "expr", epic_expr, METH_VARARGS, "Return the result of an expression (parse_inline)" }, { "call", epic_call, METH_VARARGS, "Call a function with expansion (but $* is empty) (call_function)" }, { "expand", epic_expand, METH_VARARGS, "Expand some text with $s" }, /* Lower level facilities - $-expansion NOT supported */ { "run_command", epic_run_command, METH_VARARGS, "Run an alias or builtin command" }, { "call_function", epic_call_function, METH_VARARGS, "Call an alias or builtin function" }, { "get_set", epic_get_set, METH_VARARGS, "Get a /SET value (only)" }, { "get_assign", epic_get_assign, METH_VARARGS, "Get a /ASSIGN value (only)" }, { "get_var", epic_get_assign, METH_VARARGS, "Get a variable (either /ASSIGN or /SET)" }, { "set_set", epic_set_set, METH_VARARGS, "Set a /SET value (only)" }, { "set_assign", epic_set_assign, METH_VARARGS, "Set a /ASSIGN value (only)" }, { "builtin_cmd", epic_builtin_cmd, METH_VARARGS, "Make a Python function an EPIC builtin command" }, /* Lower level IO facilities */ { "callback_when_readable", epic_callback_when_readable, METH_VARARGS, "Register a python function for FD event callbacks" }, { "callback_when_writable", epic_callback_when_writable, METH_VARARGS, "Register a python function for FD event callbacks" }, { "cancel_callback", epic_cancel_callback, METH_VARARGS, "Unregister FD event callbacks" }, { NULL, NULL, 0, NULL } }; static PyModuleDef epicModule = { PyModuleDef_HEAD_INIT, "_epic", NULL, -1, epicMethods, NULL, NULL, NULL, NULL }; /* * This is called via * PyImport_AppendInittab("_epic", &PyInit_epic); * before * PyInitialize(); * in main(). */ static PyObject * PyInit_epic (void) { return PyModule_Create(&epicModule); } void initialize_python (int start) { if (p_initialized == 0) { PyImport_AppendInittab("_epic", &PyInit_epic); Py_Initialize(); p_initialized = 1; global_vars = PyModule_GetDict(PyImport_AddModule("__main__")); init_python_fd_callbacks(); } } /***********************************************************/ /* * python_eval_expression - Call a Python Expression -- the $python() function * * Note: This can only be used to call a Python Expression. * It cannot be used to call a python statement. * * See at the top of the file -- Python makes a distinction between * statements and expressions (like ircII does) and has different * API calls for each (PyRun_String() and PyRun_SimpleString()). * * Arguments: * input - A string to pass to the Python interpreter. * It must be a python expression, or python will throw exception. * * Return Value: * The return value of the python expression, as a new_malloc()ed string. * YOU are responsible for new_free()ing the value later (if you call this * other than as the $python() function, of course) */ char * python_eval_expression (char *input) { PyObject *retval; PyObject *retval_repr; const char *r; char *retvalstr = NULL; if (p_initialized == 0) initialize_python(1); /* * https://docs.python.org/3/c-api/veryhigh.html * says that this returns NULL if an exception is raised. * So in that case, we try to handle/format the exception. */ if (!(retval = PyRun_String(input, Py_eval_input, global_vars, global_vars))) output_traceback(); /* Convert retval to a string */ retval_repr = PyObject_Repr(retval); r = PyUnicode_AsUTF8(retval_repr); retvalstr = malloc_strdup(r); Py_XDECREF(retval); Py_XDECREF(retval_repr); RETURN_MSTR(retvalstr); } /* * python_eval_statement -- Call a Python Statement -- The /PYTHON function * * Note: This can only be used to call a Python Statement. * It cannot be used to call a python expression. * * See at the top of the file -- Python makes a distinction between * statements and expressions (like ircII does) and has different * API calls for each (PyRun_String() and PyRun_SimpleString()). * * Arguments: * input - A string to pass to the Python interpreter. * It must be a python statement, or python will throw exception * * Note: It's not clear if the statement throws an exception whether or not * we can catch and format it. We'll see! */ void python_eval_statement (char *input) { if (p_initialized == 0) initialize_python(1); /* * https://docs.python.org/3/c-api/veryhigh.html * says that "returns 0 on success or -1 if an exception is raised. * If there was an error, there is no way to get the exception * information;" That's not 100% clear. So I decided to assume * that if -1 is returned, we should see if there is exception * information and output it if we can. There is the possibility * that python has just dumped the exception to stdout and that * we are out of luck for reformatting it. */ if (PyRun_SimpleString(input)) output_traceback(); } /* * The /PYTHON command: Evalulate the args as a PYTHON block and ignore the * return value of the statement. */ BUILT_IN_COMMAND(pythoncmd) { char *body; if (*args == '{') { if (!(body = next_expr(&args, '{'))) { my_error("PYTHON: unbalanced {"); return; } } else body = args; python_eval_statement(body); } /* * * Call a python module.method directly without quoting hell * The return value (if any) is ignored. * * Arguments: * orig_object - a string containing "module.method", the name of a python method * args - a string containing the argument that will be passed to "module.method" * * Return value: * A new_malloc()ed string containing the repr()d return value of the method (success) * A new_malloc()ed zero length string, if something went wrong (failure) */ char * call_python_directly (const char *orig_object, char *args) { char *object = NULL, *module = NULL, *method = NULL; PyObject *mod_py = NULL, *args_py = NULL; PyObject *pModule = NULL, *pFunc = NULL, *pArgs = NULL, *pRetVal = NULL; PyObject *retval_repr = NULL; const char *r = NULL; char *retvalstr = NULL; /* BAH! */ if (!orig_object) RETURN_EMPTY; object = LOCAL_COPY(orig_object); module = object; if (!(method = strchr(module, '.'))) { my_error("Usage: /PYDIRECT module.method arguments"); RETURN_EMPTY; } *method++ = 0; mod_py = Py_BuildValue("z", module); pModule = PyImport_Import(mod_py); Py_XDECREF(mod_py); if (pModule == NULL) { my_error("PYDIRECT: Unable to import module %s", module); goto c_p_d_error; } pFunc = PyObject_GetAttrString(pModule, method); if (pFunc == NULL) { my_error("PYDIRECT: The module %s has nothing named %s", module, method); goto c_p_d_error; } if (!PyCallable_Check(pFunc)) { my_error("PYDIRECT: The thing named %s.%s is not a function", module, method); goto c_p_d_error; } if (!(pArgs = PyTuple_New(1))) goto c_p_d_error; if (!(args_py = Py_BuildValue("z", args))) goto c_p_d_error; if ((PyTuple_SetItem(pArgs, 0, args_py))) goto c_p_d_error; args_py = NULL; /* args_py now belongs to the tuple! */ if (!(pRetVal = PyObject_CallObject(pFunc, pArgs))) goto c_p_d_error; if (!(retval_repr = PyObject_Repr(pRetVal))) goto c_p_d_error; if (!(r = PyUnicode_AsUTF8(retval_repr))) goto c_p_d_error; retvalstr = malloc_strdup(r); goto c_p_d_cleanup; c_p_d_error: output_traceback(); c_p_d_cleanup: Py_XDECREF(pArgs); Py_XDECREF(pFunc); Py_XDECREF(pModule); Py_XDECREF(pRetVal); RETURN_MSTR(retvalstr); } /* * /PYDIRECT -- Call a python module.method directly, without using * PyRun[Simple]String() [which handles syntax parsing] * * Usage: * /PYDIRECT module.method arguments * * You can only call "module.method" python functions that accept exactly * one string as its input parameters. If you need to call anything more * sophisticated than that, you need to use /PYTHON or $python() to handle * the parsing. */ BUILT_IN_COMMAND(pydirect_cmd) { char *pyfuncname; char *x; if (!args || !*args) { say("Usage: /PYDIRECT module.method arguments"); return; } pyfuncname = new_next_arg(args, &args); x = call_python_directly(pyfuncname, args); new_free(&x); } /* * output_traceback - output a python exception in an epic-friendly way * * Certain things may cause Python Exceptions. When we detect that an * exception has occurred, we call this function to handle it. * * Arguments: None * Return value : None */ void output_traceback (void) { say("The python evaluation threw an exception."); #if 0 PyObject *ptype, *pvalue, *ptraceback; PyObject *ptype_repr; char *ptype_str; PyErr_Print(); PyErr_Fetch(&ptype, &pvalue, &ptraceback); if (ptype != NULL) { ptype_repr = PyObject_Repr(ptype); ptype_str = PyUnicode_AsUTF8(ptype_repr); say("Exception Type: %s", ptype_str); } if (pvalue != NULL) { PyObject *pvalue_repr; char *pvalue_str; pvalue_repr = PyObject_Repr(pvalue); pvalue_str = PyUnicode_AsUTF8(pvalue_repr); say("Value: %s", pvalue_str); } if (ptraceback != NULL) { #if 0 PyObject *ptraceback_repr; char *ptraceback_str; ptraceback_repr = PyObject_Repr(ptraceback); ptraceback_str = PyUnicode_AsUTF8(ptraceback_repr); say("Traceback: %s", ptraceback_str); #endif } Py_XDECREF(ptype); Py_XDECREF(pvalue); Py_XDECREF(ptraceback); #endif return; }