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.
- Shows how you can interpret and handle messages coming out of it, through Python
- 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.