Python: TCPServer/Webserver for Poser (Beta)



  • I made a replacement for theTCPServer available in Python 2.7, but working with Poser. Also a replacement for the SimpleHTTPRequestHandler. Together both can be used to make a working Webserver for Poser "the easy way".

    Background: The standard-lib uses threading to exchange data between server and client. But this is not possible in Poser so TCPServer was never used in Poser (as far as I know). I use WX to interrupt what Poser is doing to simulate multitasking (with some restrictions).

    I tested this a few days now and it worked so far. Even restarting after modifying the code works here without problems. But to be sure, other people should try it out.

    This is how the server (in the demo a webserver) is started:

    if __name__ == "__main__":
        # Allow local networks by default
        ALLOWED_ADDRESSES = "192.168.*, 169.254.*, 10.*, 127.0.*"
    
        try:
            # make sure an already running server is shutdown at (re)start.
            httpd.shutdown()
        except NameError:
            pass
    
        handler = PoserHTTPRequestHandler  # the part serving http-requests
    
        # The following is the basic server listening for incoming connections. It works
        # like Pythons "SimpleHTTPServer". Basically.
        # (MY_IP is defined at the top of this script).
    
        httpd = TCPServer((MY_IP, MY_PORT), handler)
        # this is not standard but useful for security.
        httpd.set_allowed(ALLOWED_ADDRESSES)
    
        # Start the server but return (end this script). The script is kept
        # alive by automatically calling wx.CallLater() or poser.Scene().EventCallback().
        httpd.serve_forever()
    
        # You'll see this because the script does not hang after
        # calling serve_forever().
        print("Webserver '%s' started as %s" % (handler.server_version,
                                                str(httpd.server_address)))
    
        # Call httpd.shutdown() to bring down the server. You can do this
        # via another script or via Posers Python-Console.
    
    

    The "working part" is, like in Phython's original SimpleHTTPServer, part of the Requesthandler.

    The whole thing is in two files:
    P_TCPServer.py (replacement for TCPServer.py) and PoserWebserver.py containing the Requessthandler (based on the standard SimpleHTTPRequestHandler) and Webserver-Democode.

    The TCPServer can be used to make other servers too. Chat-server, RPC-Server, whatever.

    Here is P_TCPServer.py

    ##############################################################################
    ##############################################################################
    ## ##
    ## Copyright (C) 2018, Author: A::D:P (F. Hartmann, Germany)
    ##
    ## This program is free software: you can redistribute it and/or modify
    ## it under the terms of the GNU General Public License as published by
    ## the Free Software Foundation, either version 3 of the License, or
    ## (at your option) any later version.
    ##
    ## This program is distributed in the hope that it will be useful,
    ## but WITHOUT ANY WARRANTY; without even the implied warranty of
    ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ## GNU General Public License for more details.
    ##
    ## You should have received a copy of the GNU General Public License
    ## along with this program.  If not, see http://www.gnu.org/licenses/
    ##
    ##############################################################################
    #
    # Socket Server based on Pythons standard Socket Server for Poser.
    #
    ##############################################################################
    
    from __future__ import print_function
    
    import sys
    
    assert sys.version_info >= (2, 7), "At least Python version 2.7 is required."
    
    try:
        import poser
    except ImportError:
        raise RuntimeError("Needs Poser to run.")
    else:
        SCENE = poser.Scene()
    
    import re
    import socket
    
    socket.setdefaulttimeout(0.2)
    
    # import original TCPServer for subclassing
    from SocketServer import TCPServer as _TCPServer
    from time import time, asctime
    import wx
    import traceback
    
    ##############################################################################
    # Global variables
    
    # Define how "multitasking" is handled. Poser events may be used if
    # WX should not be used for any reason. Use WX if available,
    # because it is more stable.
    
    USE_WX_EVENTS = True
    USE_POSER_EVENTS = not USE_WX_EVENTS
    
    # If DEBUG is set to false, prints with debug_print() do nothing.
    DEBUG = True
    
    # if debugmessages should go to a file, set a file to write to,
    # else set to standard output (default).
    DEBUG2FILE = sys.stderr
    
    # Don't forget to open the file!
    # DEBUG2FILE = open(os.path.join(poser.ScriptLocation(), "TCPServer.log"), "w+"))
    
    ##############################################################################
    # Utility functions
    ##############################################################################
    
    def debug_print(*args):
        if DEBUG:
            print("%s:" % asctime(), *args, file=DEBUG2FILE)
            return True
        return False
    
    
    ##############################################################################
    # Classes
    ##############################################################################
    
    class TCPServer(_TCPServer):
        """
        Standard TCPServer from Python-Distri extended to run with Poser.
        Call it this way:
            server=TCPServer((<string> ip_address, <int> port), <RequestHandler>)
            e.g.:
            server=TCPServer(("127.0.0.1", 8080), HTTPRequestHandler)
            (Requesthandler must be Poser compatible)
        """
        timeout = .1
        next_calltime = 0
        call_delay = 100
        allow_reuse_address = 1
        wx_ev = None
        allowed_addresses = None
    
        def __init__(self, *args, **kw):
            self.set_allowed(kw.pop("allowed_addresses", "127.0.0.*"))
            _TCPServer.__init__(self, *args, **kw)
            self.socket.settimeout(.1)
            self.server_port = self.server_name = None
            debug_print("POSER TCPSERVER initialized")
    
        def server_bind(self):
            """Overwrite server_bind to store the server name."""
            _TCPServer.server_bind(self)
            host, port = self.socket.getsockname()[:2]
            try:
                self.server_name = socket.getfqdn(host)
            except socket.error:
                self.server_name = "Unknown"
            except Exception as err:
                if not debug_print(traceback.format_exc()):
                    raise err
    
            self.server_port = port
            self.socket.settimeout(.1)
    
        def fileno(self):
            return self.socket.fileno()
    
        def get_request(self):
            return self.socket.accept()
    
        def verify_request(self, request, client_address):
            debug_print("Connection from:", client_address)
            addr = self.allowed_addresses.match(client_address[0])
    
            if addr is None:
                debug_print("Not allowed: '%s'" % client_address[0])
                return False
            return True
    
        def set_allowed(self, allowed_adresses):
            adr = allowed_adresses.replace(".", "\.").replace("*", ".*?")
            adr = re.sub("[,\s]+", "|", adr)
            try:
                self.allowed_addresses = re.compile(adr)
            except re.error:
                debug_print("Error in regular expression '%s'.\n%s"
                            % (allowed_adresses, traceback.format_exc()))
    
        # Poser events are used if no WX is usable (or shouldn't be used).
        # Make sure no already running script using Poserevents is
        # disturbed and do not start a script that needs Poserevents
        # after you've started this server. Your server won't work anymore and
        # you may have unwanted/uninspected results at the end, up to a crash.
    
        def poser_events(self, iScene, iEventType):
            if self.next_calltime < 0:
                return
    
    #        try:
    #            self.__is_shut_down.clear()
    #        except AttributeError:
    #            pass
    
            if (iEventType <= 16) and (time() > self.next_calltime):
                self._handle_request_noblock()
                self.next_calltime = time() + (1.0 / self.call_delay)
    
        # Use WX-events if ever possible for "multitasking", e.g. keep the
        # server running beside of Poser and other Python scripts.
    
        def wx_events(self, *args):
            self._handle_request_noblock()
    
            if self.call_delay >= 0:
                self.wx_ev = wx.CallLater(self.call_delay, self.wx_events)
    
        def serve_forever(self, poll_intervall=None):
            """
            Difference between this 'serve_forever' and the original one is,
            that this version returns after the server is started. To stop
            the server, one has to use 'shutdown'.
            Returning imediately allows the server running in 'background'.
            Means, you are able to use Poser as usual and even start other
            Python scripts.
            """
            if poll_intervall is not None:
                self.call_delay = poll_intervall
            self.next_calltime = 0
    
            if poser and USE_POSER_EVENTS:
                SCENE.ClearEventCallback()
                SCENE.SetEventCallback(self.poser_events)
                debug_print("Using Poser events.")
    
            elif wx and USE_WX_EVENTS:
                self.wx_events()
                debug_print("Using wx events.")
    
            else:
                raise RuntimeError("Can't use 'serve_forever'.\n" + \
                                   "Make sure Poser is used or wx installed.")
    
        def shutdown(self):
            self.next_calltime = -1
    
            if USE_POSER_EVENTS:
                SCENE.ClearEventCallback()
            if USE_WX_EVENTS:
                if self.wx_ev is not None:
                    self.wx_ev.Stop()
                    self.wx_ev = None
    
            self.RequestHandlerClass = None
    
            try:
                self.socket.close()
            except Exception:
                pass
    
            try:
                self.socket.shutdown(socket.SHUT_RDWR)
            except Exception as err:
                pass
    
            debug_print("Server down.")
    
        def __del__(self):
            self.shutdown()
    
    

    And here is the rest.. This uses P_TCPServer as liberary.
    PoserWebserver.py

    ##############################################################################
    ##############################################################################
    ## ##
    ## Copyright (C) 2018, Author: A::D:P (F. Hartmann, Germany)
    ##
    ## This program is free software: you can redistribute it and/or modify
    ## it under the terms of the GNU General Public License as published by
    ## the Free Software Foundation, either version 3 of the License, or
    ## (at your option) any later version.
    ##
    ## This program is distributed in the hope that it will be useful,
    ## but WITHOUT ANY WARRANTY; without even the implied warranty of
    ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ## GNU General Public License for more details.
    ##
    ## You should have received a copy of the GNU General Public License
    ## along with this program.  If not, see http://www.gnu.org/licenses/
    ##
    ##############################################################################
    #
    # RequestHandler and Webserverdemo works like Pythons SimpleHTTPServer.
    #
    ##############################################################################
    from __future__ import print_function
    
    import json
    import os
    from collections import OrderedDict
    from urlparse import parse_qs
    
    try:
        import poser
    except ImportError:
        raise RuntimeError("Needs Poser to run.")
    else:
        SCENE = poser.Scene()
    
    # Change this to your likeness. Or use "" (empty string) to
    # let the machine decide which Addresses it will use
    # (IP4 or IP6)
    MY_IP = "192.168.56.100"
    # Default port the server listens for incoming connections.
    MY_PORT = 8080
    
    # import anything from modified TCPServer
    from P_TCPServer import *
    
    # import Simple HTTP requesthandler for subclassing
    from SimpleHTTPServer import SimpleHTTPRequestHandler
    
    # we serve from this directory (base-directory)
    POSER_DIR = os.path.join(poser.ContentRootLocation(), "Runtime", "Libraries")
    os.chdir(POSER_DIR)
    
    # Few simple html-pages
    HTML_PAGE = """<html>
    <title="{0}" />
    <body>
    {1}
    </body>
    </html>
    """
    
    SCREENSHOT_HTML = """<html>
    <title="Poser Screenshot" />
    <body>
    <img src="{0}" width="100%" />
    </body>
    </html>
    """
    
    
    def poser_object_to_dict(poser_obj, base_dict=OrderedDict()):
        """
        Return a dictionary containing info about a poser object.
        (just for demo, far away from perfect)
        """
        d = base_dict.setdefault(poser_obj.__class__.__name__, OrderedDict())
    
        try:
            d.setdefault("Name", dict(internal=poser_obj.InternalName(), external=poser_obj.Name()))
        except Exception as err:
            d.setdefault("Error", str(err))
        else:
            attributes = "Hidden IsMorphTarget IsValueParameter " + \
                         "MaxValue MinValue InitValue Value ForceLimits Sensitivity " + \
                         "NumMorphTargetDeltas NumValueOperations"
    
            if isinstance(poser_obj, poser.ParmType):
                # Handle parameter types
                dd = OrderedDict()
                for key in attributes.split():
                    try:
                        dd[key] = getattr(poser_obj, key)()
                    except AttributeError:
                        pass
    
                d["Parameterdata"] = dd
            else:
                # Handle all non parameter types
                if isinstance(poser_obj, poser.FigureType):
                    dd = d["BodyParts"] = list()
    
                    for bp in poser_obj.Actors():
                        if bp.IsBodyPart():
                            temp = poser_object_to_dict(bp)
                            for k, v in temp.items():
                                flag = False
                                if isinstance(v, dict):
                                    flag = True
                                    break
                            if not flag:
                                dd.append(temp)
    
                else:
                    try:  # because not every poser object has children...
                        children = OrderedDict()
                        for entry in poser_obj.Children():
                            c = children.setdefault(entry.__class__.__name__ + "s", list())
                            c.append(entry.InternalName())
                    except AttributeError:
                        pass
                    else:
                        d.setdefault("Children", children)
    
                    try:  # because some actors don't have a geometry...
                        geom = poser_obj.Geometry()
                    except Exception:
                        geom = None
    
                    if geom:
                        g = OrderedDict()
                        g.setdefault("NumVertices", geom.NumVertices())
                        g.setdefault("NumPolygons", geom.NumPolygons())
                        d.setdefault("Geometry", g)
    
        return base_dict
    
    
    def dict_to_json(d):
        try:
            return json.dumps(d)
        except Exception:
            return "<pre>%s</pre>" % repr(d)
    
    
    ##############################################################################
    ##
    ## Sample Requesthandler as demo. It shows that the standard requesthandler
    ## works as expected.
    ##
    ##############################################################################
    
    class PoserHTTPRequestHandler(SimpleHTTPRequestHandler):
        server_version = "Poser Simple Webserver 0.01"
    
        def __init__(self, *args, **kw):
            SimpleHTTPRequestHandler.__init__(self, *args, **kw)
            self.vars = dict()
            self.path = None
    
        def do_GET(self):
            """
            Serve GET request.
            self.path holds the actual request.
            """
            part, _, rest = self.path[1:].partition("/")
            part = "cmd_" + part
    
            # Find out if we have a function for the request
            try:
                if hasattr(self, part):
                    # yes, call it
                    getattr(self, part)(rest)
                else:
                    # procede with standard request
                    return SimpleHTTPRequestHandler.do_GET(self)
            except Exception as err:
                self.send_html("<pre>%s</pre>" % err)
    
        def do_POST(self):
            """Handles HTTP POST request."""
    
            try:
                # Get arguments by reading body of request.
                # We read this in chunks to avoid straining
                # socket.read(); around the 10 or 15Mb mark, some platforms
                # begin to have problems (bug #792570).
                max_chunk_size = 10 * 1024 * 1024
                size_remaining = int(self.headers["content-length"])
                L = []
                while size_remaining:
                    chunk_size = min(size_remaining, max_chunk_size)
                    chunk = self.rfile.read(chunk_size)
                    if not chunk:
                        break
                    L.append(chunk)
                    size_remaining -= len(chunk)
    
                self.vars = parse_qs("".join(L))
    
            except Exception as err:  # This should only happen if the module is buggy
                # internal error, report as HTTP server error
                debug_print(traceback.format_exc())
                self.send_response(500)
                self.send_header("Content-length", "0")
                self.end_headers()
            else:
                self.do_GET()
    
        def translate_path(self, path):
            """
            Make sure files/commands are not served below a certain path
            """
            return os.path.join(POSER_DIR, SimpleHTTPRequestHandler.translate_path(self, path))
    
        def get_vars(self):
            """
            Collect variables from client
            """
            self.vars = dict()
            if "?" in self.path:
                path, _, vars = self.path.partition("?")
                self.vars = parse_qs(vars, keep_blank_values=True)
                if "JSON" in vars:
                    self.vars = json.loads(vars["JSON"], encoding="utf-8")
    
                self.path = path
                debug_print("VARS:", self.vars)
    
        def parse_request(self):
            res = SimpleHTTPRequestHandler.parse_request(self)
            if res:
                self.get_vars()
            return res
    
        def send_html(self, htmlstr, contenttype="text/html"):
            if not isinstance(htmlstr, (str, unicode)):
                raise TypeError("htmlstr must be <type string>, not %s" % htmlstr.__class__)
            length = len(htmlstr)
            self.send_response(200, "Ok.")
            self.send_header("content-type", contenttype + "; charset=utf-8")
            self.send_header("content-Length", str(length))
            self.end_headers()
            self.wfile.write(htmlstr)
    
        #
        # Special functions from here ...
        # Anything is simple, just to try out.
        #
    
        def cmd_screenshot(self, *args):
            SCENE.CopyToClipboard()
            img = wx.BitmapDataObject()
            success = False
            if wx.TheClipboard.Open():
                success = wx.TheClipboard.GetData(img)
                wx.TheClipboard.Close()
            if success:
                bm = img.GetBitmap()
                bm.SaveFile(os.path.join(POSER_DIR, "screen.PNG"), wx.BITMAP_TYPE_PNG)
                self.send_html(SCREENSHOT_HTML.format("screen.PNG"))
            else:
                raise RuntimeError("Clipboard failed")
    
        def cmd_show(self, *args):
            """
            Send the following with the browser (url):
            http://<ip>:<port>/show?figure -> return figure data in JSON format from CurrentFigure().
            http://<ip>:<port>/show?actor -> return actor data in JSON format from CurrentActor().
            http://<ip>:<port>/show?actor=hip -> select actor hip and return actor data in JSON format.
            http://<ip>:<port>/show?param=Twist -> return Parameter 'Twist' from current actor in JSON format.
            http://<ip>:<port>/show?param=Twist&value=1 -> like above but set value to '1' too.
            http://<ip>:<port>/show?actor=hip&param=Twist&value=1 -> all of the above at once.
            """
    
            d = OrderedDict()
            vars = self.vars
            if "actor" in vars:
                ac = SCENE.ActorByInternalName(vars.get("actor")[0])
                SCENE.SelectActor(ac)
                d = poser_object_to_dict(ac, d)
    
            if "param" in vars:
                param = SCENE.CurrentActor().Parameter(vars.get("param")[0])
                if "value" in vars:
                    param.SetValue(int(vars.get("value")[0]))
                d = poser_object_to_dict(param, d)
    
            if "parameters" in vars:
                ac = SCENE.CurrentActor()
                d = dict()
                dp = d["Parameters"] = dict()
                sortednames = sorted([a.Name() for a in ac.Parameters()])
                for name in sortednames:
                    try:
                        p = ac.Parameter(name)
                        dp[p.Name()] = p.Value()
                    except Exception:
                        pass
    
            self.send_html(dict_to_json(d), "application/json")
            SCENE.DrawAll()
    
        def cmd_actor(self, *args):
            try:
                ac = SCENE.CurrentActor()
                return self.send_html(dict_to_json(poser_object_to_dict(ac)), "application/json")
            except Exception as err:
                raise err
    
        def cmd_figure(self, *args):
            fig = SCENE.CurrentFigure()
            return self.send_html(dict_to_json(poser_object_to_dict(fig)), "application/json")
    
    
    ##############################################################################
    
    if __name__ == "__main__":
        # Allow local networks by default
        ALLOWED_ADDRESSES = "192.168.*, 169.254.*, 10.*, 127.0.*"
    
        try:
            # make sure an already running server is shutdown at (re)start.
            httpd.shutdown()
        except NameError:
            pass
    
        handler = PoserHTTPRequestHandler  # the part serving http-requests
    
        # This is the basic server listening for incoming connections. It works
        # like Pythons "SimpleHTTPServer". Basically.
        # (MY_IP is defined at the top of this script).
    
        httpd = TCPServer((MY_IP, MY_PORT), handler)
        # this is not standard but useful for security.
        httpd.set_allowed(ALLOWED_ADDRESSES)
    
        # Start the server but return (end this script). The script is kept
        # alive by automatically calling wx.CallLater() or poser.Scene().EventCallback().
        httpd.serve_forever()
    
        # You'll see this because the script does not hang after
        # calling serve_forever().
        print("Webserver '%s' started as %s" % (handler.server_version,
                                                str(httpd.server_address)))
    
        # Call httpd.shutdown() to bring down the server. You can do this
        # via another script or via Posers Python-Console.
    
    

  • Poser Ambassadors

    This is interesting to me. Years ago I made a web server in Poser which I then used to create useful UI elements as dynamic web pages. Basically single-page applications. Since then we got wx which made some of the value proposition of that somewhat diminished.

    What do you use this for?



  • I made this webserver more or less to get a working TCP Standard-Library. Some people are used to it.

    To transport (Poser) data between machines I'm using another TCP-Lib. Less overhead, faster. Here is how an Echo-Server looks like with this lib:

    if __name__ == "__main__":
        class EchoServer(PoserTCP):
            def __init__(self, host="192.168.56.100", port=8080, allowed_ips=ALLOWED_IPS):
                super(EchoServer, self).__init__(host, port, allowed_ips)
    
            def on_connected(self, other_ip):
                debug_print("on_connected: from", other_ip)
                self.write("Echoserver based on Poser." + self.EOL)
    
            def on_data_available(self):
                debug_print("DATA AVAILABLE")
                debug_print(self.inBuffer)
                
                while self.data_available:
                    line = self.readline()
                    if line is None:
                        break
    
                    line = "Echo: " + line.strip()
                    self.write(line+self.EOL)
    
            def on_connection_lost(self):
                debug_print("Connection to client lost")
    
    

    Im using this TCPServer primarily to make bridges (Poser <-> OtherApps). In combination with an UDP-Server. Roughly: My Poser broadcasts some of his (and some custom-made) events to the local net, OtherApp then connects via TCP for data transfer. And my Poser can react on UDP messages too.

    Then, if I find time for it, I try to get my Android devices useful used with Poser. Beside of just displaying a few buttons to activate commands.