Skip to content

Commit

Permalink
Use fake stdout in server to allow cloning output to clients
Browse files Browse the repository at this point in the history
  • Loading branch information
MaddyGuthridge committed Jan 10, 2024
1 parent 367e411 commit d885f4b
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 30 deletions.
20 changes: 20 additions & 0 deletions flapi/__comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ def send_msg(msg: bytes):
getContext().port.send(mido_msg)


def handle_stdout(output: bytes):
print(output.decode(), end='')


def handle_received_message(msg: bytes) -> Optional[bytes]:
"""
Handling of some received MIDI messages. If the event is a response to an
Expand All @@ -93,6 +97,11 @@ def handle_received_message(msg: bytes) -> Optional[bytes]:
):
return None

# Handle FL Studio stdout
if msg.removeprefix(consts.SYSEX_HEADER)[1] == consts.MSG_TYPE_STDOUT:
handle_stdout(msg.removeprefix(consts.SYSEX_HEADER)[3:])
return None

# Normal processing
return msg[len(consts.SYSEX_HEADER) + 1:]

Expand Down Expand Up @@ -227,3 +236,14 @@ def fl_eval(expression: str) -> Any:

# Value is ok, eval and return it
return eval(response[2:])


def fl_print(text: str):
"""
Print the given text to FL Studio's Python console.
"""
send_msg(
consts.SYSEX_HEADER
+ bytes([consts.MSG_FROM_CLIENT, consts.MSG_TYPE_STDOUT])
+ text.encode()
)
5 changes: 5 additions & 0 deletions flapi/__consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@
Otherwise, the `repr()` of the return value is encoded.
"""

MSG_TYPE_STDOUT = 0x04
"""
Message contains text to write into stdout
"""

MSG_STATUS_OK = 0x00
"""
Message was processed correctly.
Expand Down
17 changes: 9 additions & 8 deletions flapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Remotely control FL Studio using the MIDI Controller Scripting API.
"""
from .__enable import enable, init, disable
from .__comms import heartbeat, fl_exec, fl_eval
from .__comms import heartbeat, fl_exec, fl_eval, fl_print
from . import errors
from .__consts import VERSION

Expand All @@ -14,11 +14,12 @@


__all__ = [
'enable',
'init',
'disable',
'heartbeat',
'fl_exec',
'fl_eval',
'errors',
"enable",
"init",
"disable",
"heartbeat",
"fl_exec",
"fl_eval",
"fl_print",
"errors",
]
2 changes: 1 addition & 1 deletion flapi/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
A simple program to run Flapi commands
"""
import click
from click_default_group import DefaultGroup
from click_default_group import DefaultGroup # type: ignore
from .cli import install_main, repl_main, uninstall_main
from .cli import consts
from pathlib import Path
Expand Down
20 changes: 15 additions & 5 deletions flapi/cli/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import code
from typing import Optional
from traceback import print_exception
from flapi import enable, init, disable, heartbeat, fl_exec, fl_eval
from flapi import enable, init, disable, heartbeat, fl_exec, fl_eval, fl_print
import flapi
try:
import IPython
Expand All @@ -27,29 +27,37 @@
"heartbeat": heartbeat,
"fl_exec": fl_exec,
"fl_eval": fl_eval,
"fl_print": fl_print,
}


def exec_lines(lines: list[str]):
def exec_lines(lines: list[str], is_statement: bool):
"""
Execute the given lines on the server
"""
code = "\n".join(lines)
if code == "exit":
exit()
try:
fl_exec(code)
if is_statement:
fl_exec(code)
else:
res = fl_eval(code)
print(repr(res))
except Exception as e:
print_exception(e)


def start_server_shell():
"""
A simple REPL where all code is run server-side
Note: this is very very buggy and probably shouldn't be relied upon.
"""
print("Type `exit` to quit")

lines = []
is_statement = False
is_indented = False
curr_prompt = ">>> "

Expand All @@ -60,18 +68,20 @@ def start_server_shell():
# If we're not indented, check if the next line will be
if line.strip().endswith(":"):
is_indented = True
is_statement = True

# If we are indented, only an empty line can end the statement
if is_indented:
if line == "":
exec_lines(lines)
exec_lines(lines, is_statement)
lines = []
is_indented = False
is_statement = False
curr_prompt = ">>> "
else:
curr_prompt = "... "
else:
exec_lines(lines)
exec_lines(lines, is_statement)
lines = []
curr_prompt = ">>> "

Expand Down
5 changes: 5 additions & 0 deletions flapi/script/__consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@
Otherwise, the `repr()` of the return value is encoded.
"""

MSG_TYPE_STDOUT = 0x04
"""
Message contains text to write into stdout
"""

MSG_STATUS_OK = 0x00
"""
Message was processed correctly.
Expand Down
90 changes: 74 additions & 16 deletions flapi/script/device_flapi_server.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,65 @@
# name=Flapi Server
# supportedDevices=Flapi
import device
import sys
import __consts as consts
try:
from fl_classes import FlMidiMsg
except ImportError:
pass
try:
# This is the module in most Python installs, used for type safety
from io import StringIO
except ImportError:
# This is the module in FL Studio for some reason
from _io import StringIO # type: ignore


def init_fake_stdout():
"""
Initialize a fake buffer for stdout
"""
global fake_stdout
fake_stdout = StringIO()
sys.stdout = fake_stdout


real_stdout = sys.stdout
fake_stdout = StringIO()
init_fake_stdout()


def display_stdout():
"""
Display the contents of stdout in FL Studio's console, then clear the
buffer
"""
fake_stdout.seek(0)
print(fake_stdout.read(), file=real_stdout)
init_fake_stdout()


def send_stdout():
"""
Send the contents of stdout to the client's console, then clear the buffer
"""
fake_stdout.seek(0)
text = fake_stdout.read()
send_ok_with_data(consts.MSG_TYPE_STDOUT, text)
init_fake_stdout()


def OnInit():
print("Flapi server")
print(f"Server version: {'.'.join(str(n) for n in consts.VERSION)}")
print(f"Device name: {device.getName()}")
print(f"Device assigned: {bool(device.isAssigned())}")
print(f"FL Studio port number: {device.getPortNumber()}")
print(
"\n".join([
"Flapi server",
f"Server version: {'.'.join(str(n) for n in consts.VERSION)}",
f"Device name: {device.getName()}",
f"Device assigned: {bool(device.isAssigned())}",
f"FL Studio port number: {device.getPortNumber()}",
]),
file=real_stdout,
)


def bytes_to_str(msg: bytes) -> str:
Expand All @@ -23,7 +69,7 @@ def bytes_to_str(msg: bytes) -> str:
return f"{repr([hex(i) for i in msg])} ({repr(msg)})"


def respond_ok(msg_type: int):
def send_ok(msg_type: int):
"""
Respond to a message with an OK status
"""
Expand All @@ -39,7 +85,7 @@ def respond_ok(msg_type: int):
)


def respond_ok_with_data(msg_type: int, data: 'str | bytes'):
def send_ok_with_data(msg_type: int, data: 'str | bytes'):
"""
Respond to a message with an OK status, additionally attaching the given
data.
Expand All @@ -60,7 +106,7 @@ def respond_ok_with_data(msg_type: int, data: 'str | bytes'):
)


def respond_err(msg_type: int, error: Exception):
def send_err(msg_type: int, error: Exception):
"""
Respond to a message with an ERR status
"""
Expand All @@ -77,7 +123,7 @@ def respond_err(msg_type: int, error: Exception):
)


def respond_fail(msg_type: int, message: str):
def send_fail(msg_type: int, message: str):
"""
Respond to a message with a FAIL status
"""
Expand All @@ -98,14 +144,14 @@ def heartbeat():
"""
Received a heartbeat message
"""
return respond_ok(consts.MSG_TYPE_HEARTBEAT)
return send_ok(consts.MSG_TYPE_HEARTBEAT)


def version_query():
"""
Return the version of the Flapi server
"""
return respond_ok_with_data(
return send_ok_with_data(
consts.MSG_TYPE_VERSION_QUERY,
bytes(consts.VERSION),
)
Expand All @@ -120,10 +166,11 @@ def fl_exec(code: str):
exec(code, globals())
except Exception as e:
# Something went wrong, give the error
return respond_err(consts.MSG_TYPE_EXEC, e)
return send_err(consts.MSG_TYPE_EXEC, e)

# Operation was a success, give response
return respond_ok(consts.MSG_TYPE_EXEC)
send_stdout()
return send_ok(consts.MSG_TYPE_EXEC)


def fl_eval(expression: str):
Expand All @@ -135,10 +182,18 @@ def fl_eval(expression: str):
result = eval(expression, globals())
except Exception as e:
# Something went wrong, give the error
return respond_err(consts.MSG_TYPE_EXEC, e)
return send_err(consts.MSG_TYPE_EVAL, e)

# Operation was a success, give response
return respond_ok_with_data(consts.MSG_TYPE_EVAL, repr(result))
send_stdout()
return send_ok_with_data(consts.MSG_TYPE_EVAL, repr(result))


def receive_stdout(text: str):
"""
Receive text from client, and display it in FL Studio's console
"""
print(text, end='', file=real_stdout)


def OnSysEx(event: 'FlMidiMsg'):
Expand Down Expand Up @@ -168,4 +223,7 @@ def OnSysEx(event: 'FlMidiMsg'):
if message_type == consts.MSG_TYPE_EVAL:
return fl_eval(data[2:].decode())

respond_fail(message_type, f"Unknown message type {message_type}")
if message_type == consts.MSG_TYPE_STDOUT:
return receive_stdout(data[2:].decode())

send_fail(message_type, f"Unknown message type {message_type}")

0 comments on commit d885f4b

Please sign in to comment.