Skip to content

Commit

Permalink
Implement much improved system for stdout capture
Browse files Browse the repository at this point in the history
  • Loading branch information
MaddyGuthridge committed Jan 11, 2024
1 parent 7fcf855 commit 4cfd40c
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 59 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"Capout",
"Flapi",
"mido"
]
Expand Down
9 changes: 5 additions & 4 deletions flapi/__comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
The data format used to communicate is quite simple, containing only a small
number of bytes
### Sysex header (as found in the `__consts` module)
### Sysex header (as found in the `_consts` module)
Used to ensure that the message originates from Flapi's systems.
Expand Down Expand Up @@ -52,8 +52,9 @@
import time
from mido import Message as MidoMsg # type: ignore
from typing import Any, Optional
from .__util import try_eval
from .__context import getContext
from . import __consts as consts
from flapi import _consts as consts
from .errors import (
FlapiTimeoutError,
FlapiInvalidMsgError,
Expand Down Expand Up @@ -125,7 +126,7 @@ def assert_response_is_ok(msg: bytes, expected_msg_type: int):
if msg_status == consts.MSG_STATUS_OK:
return
elif msg_status == consts.MSG_STATUS_ERR:
raise eval(msg[2:])
raise try_eval(msg[2:])
elif msg_status == consts.MSG_STATUS_FAIL:
raise FlapiServerError(msg[2:].decode())

Expand Down Expand Up @@ -235,7 +236,7 @@ def fl_eval(expression: str) -> Any:
assert_response_is_ok(response, consts.MSG_TYPE_EVAL)

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


def fl_print(text: str):
Expand Down
2 changes: 1 addition & 1 deletion flapi/__decorate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing_extensions import ParamSpec
from functools import wraps
from .__comms import fl_eval
from .__consts import FL_MODULES
from ._consts import FL_MODULES

P = ParamSpec('P')
R = TypeVar('R')
Expand Down
8 changes: 4 additions & 4 deletions flapi/__enable.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from time import sleep
from typing import Protocol, Generic, TypeVar, Optional
from mido.ports import BaseOutput, BaseInput, IOPort # type: ignore
from . import __consts as consts
from . import _consts as _consts
from .__context import setContext, popContext, FlapiContext
from .__comms import poll_for_message, fl_exec, heartbeat, version_query
from .__decorate import restore_original_functions, add_wrappers
Expand Down Expand Up @@ -51,7 +51,7 @@ def open_port(
return None


def enable(port_name: str = consts.DEFAULT_PORT_NAME) -> bool:
def enable(port_name: str = _consts.DEFAULT_PORT_NAME) -> bool:
"""
Enable Flapi, connecting it to the given MIDI ports
Expand Down Expand Up @@ -124,7 +124,7 @@ def init():
version_check()

# Finally, import all of the required modules in FL Studio
for mod in consts.FL_MODULES:
for mod in _consts.FL_MODULES:
fl_exec(f"import {mod}")


Expand All @@ -135,7 +135,7 @@ def version_check():
If not, raise an exception.
"""
server_version = version_query()
client_version = consts.VERSION
client_version = _consts.VERSION

if server_version < client_version:
raise FlapiVersionError(
Expand Down
2 changes: 1 addition & 1 deletion flapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .__enable import enable, init, disable
from .__comms import heartbeat, fl_exec, fl_eval, fl_print
from . import errors
from .__consts import VERSION
from ._consts import VERSION


__version__ = ".".join(str(n) for n in VERSION)
Expand Down
14 changes: 14 additions & 0 deletions flapi/__util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,24 @@
Helper functions
"""
from typing import Any


def bytes_to_str(msg: bytes) -> str:
"""
Helper to give a nicer representation of bytes
"""
return f"{repr([hex(i) for i in msg])} ({repr(msg)})"


def try_eval(source: str | bytes) -> Any:
"""
Evaluate the given source code, but raise a sane exception if it fails
"""
if isinstance(source, bytes):
source = source.decode()
try:
return eval(source)
except Exception as e:
raise ValueError(
f"Unable to evaluate code {repr(source)}, got error {repr(e)}")
File renamed without changes.
146 changes: 146 additions & 0 deletions flapi/script/capout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
# Flapi > Script > Capout
Simple class for managing the capturing of stdout in FL Studio.
TODO: Set up a callback to be triggered whenever a
"""
import sys
try:
# This is the module in most Python installs, used for type safety
from io import StringIO, TextIOBase
except ImportError:
# This is the module in FL Studio for some reason
from _io import StringIO, _TextIOBase as TextIOBase # type: ignore
try:
from typing import Optional, Callable
except ImportError:
pass


print("hi")


class CapoutBuffer(TextIOBase): # type: ignore
"""
Custom buffer wrapping a StringIO, so that we can implement a callback
whenever buffer is flushed, and flush it to the client.
This is probably awful design, but it seems to work so I'm keeping it until
I feel like writing something nicer.
"""
def __init__(self, callback: 'Callable[[str], None]') -> None:
self.__callback = callback
self.__buf = StringIO()

def close(self):
return self.__buf.close()

@property
def closed(self) -> bool:
return self.__buf.closed

def fileno(self) -> int:
return self.__buf.fileno()

def flush(self) -> None:
self.__buf.flush()
self.__buf.seek(0)
text = self.__buf.read()
self.__callback(text)
self.__buf = StringIO()

def isatty(self) -> bool:
return self.__buf.isatty()

def readable(self) -> bool:
return self.__buf.readable()

def readline(self, size=-1, /) -> str:
return self.__buf.readline(size)

def readlines(self, hint=-1, /) -> list[str]:
return self.__buf.readlines(hint)

def seek(self, offset: int, whence=0, /) -> int:
return self.__buf.seek(offset, whence)

def seekable(self) -> bool:
return self.__buf.seekable()

def tell(self) -> int:
return self.__buf.tell()

def truncate(self, size: 'Optional[int]' = None, /) -> int:
return self.__buf.truncate(size)

def writable(self) -> bool:
return self.__buf.writable()

def writelines(self, lines: list[str], /) -> None:
return self.__buf.writelines(lines)

@property
def encoding(self):
return self.__buf.encoding

@property
def errors(self):
return self.__buf.errors

@property
def newlines(self):
return self.__buf.newlines

@property
def buffer(self):
return self.__buf.buffer

def detach(self):
return self.__buf.detach()

def read(self, size=-1, /) -> str:
return self.__buf.read(size)

def write(self, s: str, /) -> int:
return self.__buf.write(s)


print("hi2")


class Capout:
"""
Capture stdout in FL Studio
"""
def __init__(self, callback: 'Callable[[str], None]') -> None:
self.enabled = False
self.real_stdout = sys.stdout
self.fake_stdout = CapoutBuffer(callback)

def flush(self) -> None:
if self.enabled:
self.fake_stdout.flush()

def enable(self):
self.enabled = True
sys.stdout = self.fake_stdout

def disable(self):
self.enabled = False
sys.stdout = self.real_stdout

def fl_print(self, *args, **kwargs) -> None:
"""
Print to FL Studio's output
"""
print(*args, **kwargs, file=self.real_stdout)

def client_print(self, *args, **kwargs) -> None:
"""
Print to the client's output
"""
print(*args, **kwargs, file=self.fake_stdout)


print("hi3")
File renamed without changes.
66 changes: 17 additions & 49 deletions flapi/script/device_flapi_server.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,33 @@
# name=Flapi Server
# supportedDevices=Flapi
import device
import sys
import __consts as consts
import consts
from capout import Capout
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():
def send_stdout(text: str):
"""
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():
Callback for Capout, sending stdout to the client console
"""
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()
send_ok_with_data(consts.MSG_TYPE_STDOUT, text)


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()
capout = Capout(send_stdout)
capout.enable()


def OnInit():
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,
)
capout.fl_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()}",
]))


def bytes_to_str(msg: bytes) -> str:
Expand Down Expand Up @@ -169,7 +137,7 @@ def fl_exec(code: str):
return send_err(consts.MSG_TYPE_EXEC, e)

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


Expand All @@ -185,15 +153,15 @@ def fl_eval(expression: str):
return send_err(consts.MSG_TYPE_EVAL, e)

# Operation was a success, give response
send_stdout()
capout.flush()
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)
capout.fl_print(text, end='')


def OnSysEx(event: 'FlMidiMsg'):
Expand Down

0 comments on commit 4cfd40c

Please sign in to comment.