Floating Server Web API

Hi all,

For you fellow floating licence users, I’ve got something for you. At the moment, whenever someone checks out a licence from your licence server, it’ll print a message along the lines of…

“Hey, this person with this IP just leased a licence”

Which is how you can stay up to date with how many seats are left and who’s using the ones that are occupied. But it didn’t really extend beyond that. If you didn’t have direct access to the terminal or log file of this licence server then you’d never know what’s up.

So I’ve put together a Python wrapper around the licence server that does two things.

  1. Shows how you can interpret and handle messages coming out of it, through Python
  2. Enables anyone to query the current state of it via a browser

The “website” it serves for you is incredibly rudimentary and looks like this.

{"used": {"IP=82.15.122.2": ["2022-05-12, 21:46:41", "someUser"]}, "remaining": 4}

And yep, that’s JSON.

As a bonus, there’s an example of how to connect via a second Python instance, to fetch the data as plain JSON that can be consumed and used in a e.g. widget. Odds are I’ll incorporate this into the actual Ragdoll UI at some point, such that the end user can see how many seats there are left without having to ask.

So without further ado, here’s how to use it.

python turbofloat.py
# Listening for turbofloat messages..
# Web API @ localhost:8002
# --> someUser leased a licence, 4 remaining
# --> someUser dropped a lease, 5 remaining

turbofloat.py

import json
import threading
import subprocess
import socketserver

# This is where remote machines will connect to get licence details
DEFAULT_PORT = 8002
DEFAULT_EXE = ".\TurboFloatServer.exe"

state = {
    "used": {},  # ip: (date, user)
    "remaining": -1,
}

def on_new_lease_assigned(date, msg):
    a, b, c = msg.split(". ")[:3]
    # a) New lease assigned (konst, 1, IP=::1, PID=7936)
    # b) Expires: 2022-05-12 18:06:01 (in UTC)
    # c) Used / Total leases: 1 / 5

    user, _, ip, _ = a.split("(", 1)[-1].rstrip(")").split(", ")[:4]
    _, used = c.rsplit(": ")
    used, total = map(int, used.split(" / "))

    state["used"][ip] = (date, user)
    state["remaining"] = total - used

    print("--> %s leased a licence, %d remaining" % (user, state["remaining"]))


def on_lease_released(date, msg):
    a, b = msg.split(". ")[:2]
    # a) Lease was released by client (konst, 1, IP=::1, PID=7936)
    # b) Used / Total leases: 0 / 5

    user, _, ip, _ = a.split("(", 1)[-1].rstrip(")").split(", ")[:4]
    _, used = b.rsplit(": ")
    used, total = map(int, used.split(" / "))

    state["used"].pop(ip, None)
    state["remaining"] = total - used

    print("--> %s dropped a lease, %d remaining" % (user, state["remaining"]))


def on_other_message(date, msg):
    print(msg)


def on_message(line, verbose=False):
    date, msg = line.split(">: ")
    date, level = date.split(" <")

    if msg.lower().startswith("new lease"):
        on_new_lease_assigned(date, msg)

    elif msg.lower().startswith("existing lease loaded"):
        on_new_lease_assigned(date, msg)

    elif msg.lower().startswith("lease was released"):
        on_lease_released(date, msg)

    elif msg.lower().startswith("lease has expired"):
        on_lease_released(date, msg)

    elif verbose:
        on_other_message(date, msg)


class WebApiHandler(socketserver.StreamRequestHandler):
    """Send `state` to anyone connecting to us via a browser"""

    def handle(self):
        # No matter what the client asks for, we'll
        # send the state as JSON data
        data = bytes(json.dumps(state), "utf-8")

        req = self.rfile.readline().strip()
        if req != b"api":
            # Make a browser understand ths response
            self.wfile.write(b"HTTP/1.1 200 OK\n")
            self.wfile.write(b"\n")

        self.wfile.write(data)


def web_api(opts):
    with socketserver.TCPServer(("", opts.port), WebApiHandler) as httpd:
        print("Web API @ localhost:%d" % opts.port)
        httpd.serve_forever()


def main(opts):
    popen = subprocess.Popen(opts.exe + " -x",
                             stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT,
                             universal_newlines=True)

    # Listen to incoming connections
    thread = threading.Thread(target=web_api, args=(opts,), daemon=True)
    thread.start()

    def listen():
        print("Listening for turbofloat messages..")

        for line in iter(popen.stdout.readline, b""):
            line = line.rstrip()  # Remove newline

            if ">: " not in line:
                # Anything of significance will have a <level>:
                continue

            try:
                on_message(line, opts.verbose)

            except Exception:
                # Should never happen, but you never know
                import traceback
                traceback.print_exc()
                print("# Could not grok this line:")
                print("# %s" % line)

    try:
        listen()
    finally:
        # On Ctrl+C or any other signs of exit,
        # take the turbofloat instance along with you
        popen.kill()

    print("All done, good bye")



if __name__ == '__main__':
    import sys
    assert sys.version_info[0] == 3, "Python %d != 3" % sys.version_info[0]

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--verbose", action="store_true", help="More messages")
    parser.add_argument("--port", type=int, default=DEFAULT_PORT,
                        help="Port used by a client connecting to this server")
    parser.add_argument("--exe", default=DEFAULT_EXE,
                        help="Full path to turbofloat binary, "
                             "e.g. TurboFloatServer.exe")

    opts = parser.parse_args()

    main(opts)

Once running, you’ll be able to browse to http://localhost:8002 to witness your lease people. Until there is at least one lease acquired, it won’t know how many there are in total. So to “prime” your licence server API, you can quickly lease and drop a licence from mayapy.

from ragdoll import licence
licence.install()
licence.drop_lease()

Next, here’s how you can connect from another Python instance, including mayapy.

client.py

"""Example of how to query the licence server

- Modify HOST to point to the licence server
- Modify PORT to correspond with the one broadcasted
by the licence server.

"""

import sys
import json
import socket

HOST = "localhost"
PORT = 8002

# A plain and simple connection, nothing is sent
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect((HOST, PORT))
    sock.sendall(bytes("api\n", "utf-8"))
    received = str(sock.recv(1024), "utf-8")

# The return value is a JSON-formatted dict,
# # the `state` variable from turbofloat.py
state = json.loads(received)

# Some test printing
print("%d licences remain" % state["remaining"])
for ip, (date, user) in state["used"].items():
    print("%s - %s : %s" % (ip, date, user))

And finally finally, here’s a quick script you can use to spin up a new Maya instance and fetch a licence, to test your licence server.

test.py

import os
os.environ["RAGDOLL_FLOATING"] = "localhost:8001"

from maya import standalone
print("Initialising..")
standalone.initialize()
from maya import cmds
print("Acquiring licence..")
cmds.loadPlugin("ragdoll")
from ragdoll import licence
licence.install()
print("Sleeping for 10 seconds..")
import time
time.sleep(10)
print("Unloading..")
cmds.file(new=True, force=True)
cmds.unloadPlugin("ragdoll")
standalone.uninitialize()
print("Quitting..")
cmds.quit()
print("Done")

Next Steps

I’ll probably update these scripts and refine them for eventual distribution alongside Ragdoll. But for the time being they should help you get going with your own internal licence monitoring. Make sure to edit the IP and port numbers in these scripts, along with swapping out the Windows executable I’ve used here for your Linux or MacOS equivalent.

Updated the above turbofloat.py script to handle expired leases, and customisable port and path to the TurboFloatServer.exe. Thanks to James Pearson for the frequent patches!

Any issues with the above, this is the place to report it.

A much simpler approach I’ve since adopted is to merely inspect the last few rows of the tfs-log.txt file that the server automatically writes to and updates whenever anything changes. It is located next to the server executable, the location can be configured in the config file as well.

On Linux, this could be achieved via the tail command for example. For all platforms, it also avoids having to wrap your server in a Python process up-front, as the file can be inspected independently of the server and after it has started.