diff --git a/flapi/__comms.py b/flapi/__comms.py index f8f519b..7c60802 100644 --- a/flapi/__comms.py +++ b/flapi/__comms.py @@ -50,6 +50,7 @@ `repr` doesn't provide a complete reconstruction) cannot be shared. """ import time +import logging from mido import Message as MidoMsg # type: ignore from typing import Any, Optional from .__util import try_eval @@ -63,6 +64,9 @@ ) +log = logging.getLogger(__name__) + + def send_msg(msg: bytes): """ Send a message to FL Studio @@ -71,8 +75,8 @@ def send_msg(msg: bytes): getContext().port.send(mido_msg) -def handle_stdout(output: bytes): - print(output.decode(), end='') +def handle_stdout(output: str): + print(output, end='') def handle_received_message(msg: bytes) -> Optional[bytes]: @@ -84,11 +88,13 @@ def handle_received_message(msg: bytes) -> Optional[bytes]: # Handle universal device enquiry if msg == consts.DEVICE_ENQUIRY_MESSAGE: # Send the response + log.debug('Received universal device enquiry') send_msg(consts.DEVICE_ENQUIRY_RESPONSE) return None # Handle invalid message types if not msg.startswith(consts.SYSEX_HEADER): + log.debug('Received unrecognised message') raise FlapiInvalidMsgError(msg) # Handle loopback (prevent us from receiving our own messages) @@ -100,12 +106,16 @@ def handle_received_message(msg: bytes) -> Optional[bytes]: # Handle FL Studio stdout if msg.removeprefix(consts.SYSEX_HEADER)[1] == consts.MSG_TYPE_STDOUT: - handle_stdout(msg.removeprefix(consts.SYSEX_HEADER)[3:]) + text = msg.removeprefix(consts.SYSEX_HEADER)[3:].decode() + log.debug(f"Received server stdout: {text}") + handle_stdout(text) return None # Handle exit command if msg.removeprefix(consts.SYSEX_HEADER)[1] == consts.MSG_TYPE_EXIT: - exit(int(msg.removeprefix(consts.SYSEX_HEADER)[3:].decode())) + code = int(msg.removeprefix(consts.SYSEX_HEADER)[3:].decode()) + log.info(f"Received exit command with code {code}") + exit(code) # Normal processing return msg[len(consts.SYSEX_HEADER) + 1:] @@ -122,8 +132,16 @@ def assert_response_is_ok(msg: bytes, expected_msg_type: int): msg_type = msg[0] if msg_type != expected_msg_type: + expected = consts.MSG_TYPE_NAMES.get( + expected_msg_type, + str(expected_msg_type), + ) + actual = consts.MSG_TYPE_NAMES.get( + msg_type, + str(msg_type), + ) raise FlapiClientError( - f"Expected message type {expected_msg_type}, received {msg_type}") + f"Expected message type '{expected}', received '{actual}'") msg_status = msg[1] @@ -180,6 +198,7 @@ def heartbeat() -> bool: If no data is received, this function returns `False`. """ + log.debug("heartbeat") try: send_msg(consts.SYSEX_HEADER + bytes([ consts.MSG_FROM_CLIENT, @@ -187,8 +206,10 @@ def heartbeat() -> bool: ])) response = receive_message() assert_response_is_ok(response, consts.MSG_TYPE_HEARTBEAT) + log.debug("heartbeat: passed") return True except FlapiTimeoutError: + log.debug("heartbeat: failed") return False @@ -196,11 +217,13 @@ def version_query() -> tuple[int, int, int]: """ Query and return the version of Flapi installed to FL Studio. """ + log.debug("version_query") send_msg( consts.SYSEX_HEADER + bytes([consts.MSG_FROM_CLIENT, consts.MSG_TYPE_VERSION_QUERY]) ) response = receive_message() + log.debug("version_query: got response") assert_response_is_ok(response, consts.MSG_TYPE_VERSION_QUERY) @@ -215,12 +238,14 @@ def fl_exec(code: str) -> None: """ Output Python code to FL Studio, where it will be executed. """ + log.debug(f"fl_exec: {code}") send_msg( consts.SYSEX_HEADER + bytes([consts.MSG_FROM_CLIENT, consts.MSG_TYPE_EXEC]) + code.encode() ) response = receive_message() + log.debug("fl_exec: got response") assert_response_is_ok(response, consts.MSG_TYPE_EXEC) @@ -230,12 +255,14 @@ def fl_eval(expression: str) -> Any: Output a Python expression to FL Studio, where it will be evaluated, with the result being returned. """ + log.debug(f"fl_eval: {expression}") send_msg( consts.SYSEX_HEADER + bytes([consts.MSG_FROM_CLIENT, consts.MSG_TYPE_EVAL]) + expression.encode() ) response = receive_message() + log.debug("fl_eval: got response") assert_response_is_ok(response, consts.MSG_TYPE_EVAL) @@ -247,6 +274,7 @@ def fl_print(text: str): """ Print the given text to FL Studio's Python console. """ + log.debug(f"fl_print (not expecting response): {text}") send_msg( consts.SYSEX_HEADER + bytes([consts.MSG_FROM_CLIENT, consts.MSG_TYPE_STDOUT]) diff --git a/flapi/__decorate.py b/flapi/__decorate.py index 1bcc143..43aa2fb 100644 --- a/flapi/__decorate.py +++ b/flapi/__decorate.py @@ -3,9 +3,10 @@ Code for decorating the FL Studio API libraries to enable Flapi """ -from types import FunctionType +import logging import inspect import importlib +from types import FunctionType from typing import Callable, TypeVar from typing_extensions import ParamSpec from functools import wraps @@ -15,6 +16,8 @@ P = ParamSpec('P') R = TypeVar('R') +log = logging.getLogger(__name__) + ApiCopyType = dict[str, dict[str, FunctionType]] @@ -49,11 +52,10 @@ def add_wrappers() -> ApiCopyType: For each FL Studio module, replace its items with a decorated version that evaluates the function inside FL Studio. """ + log.info("Adding wrappers to API stubs") modules: ApiCopyType = {} - for mod_name in FL_MODULES: - modules[mod_name] = {} mod = importlib.import_module(mod_name) @@ -74,6 +76,7 @@ def restore_original_functions(backup: ApiCopyType): Restore the original FL Studio API Stubs functions - called when deactivating Flapi. """ + log.info("Removing wrappers from API stubs") for mod_name, functions in backup.items(): mod = importlib.import_module(mod_name) diff --git a/flapi/__enable.py b/flapi/__enable.py index 5760f11..fa4603b 100644 --- a/flapi/__enable.py +++ b/flapi/__enable.py @@ -3,6 +3,7 @@ Code for initializing/closing Flapi """ +import logging import mido # type: ignore from typing import Protocol, Generic, TypeVar, Optional from mido.ports import BaseOutput, BaseInput, IOPort # type: ignore @@ -13,6 +14,9 @@ from .errors import FlapiPortError, FlapiConnectionError, FlapiVersionError +log = logging.getLogger(__name__) + + T = TypeVar('T', BaseInput, BaseOutput, covariant=True) @@ -65,11 +69,24 @@ def enable(port_name: str = _consts.DEFAULT_PORT_NAME) -> bool: will need to call `init()` once FL Studio is running and configured correctly. """ + log.info(f"Enable Flapi client on port '{port_name}'") # First, connect to all the MIDI ports - res = open_port( - port_name, mido.get_input_names(), mido.open_input) # type: ignore - req = open_port( - port_name, mido.get_output_names(), mido.open_output) # type: ignore + inputs = mido.get_input_names() # type: ignore + outputs = mido.get_output_names() # type: ignore + + log.info(f"Available inputs are: {inputs}") + log.info(f"Available outputs are: {outputs}") + + try: + res = open_port(port_name, inputs, mido.open_input) # type: ignore + except Exception: + log.exception("Error when connecting to input") + raise + try: + req = open_port(port_name, outputs, mido.open_output) # type: ignore + except Exception: + log.exception("Error when connecting to output") + raise if res is None or req is None: try: @@ -79,6 +96,7 @@ def enable(port_name: str = _consts.DEFAULT_PORT_NAME) -> bool: ) except NotImplementedError as e: # Port could not be opened + log.exception("Could not open create new port") raise FlapiPortError(port_name) from e else: port = IOPort(res, req) diff --git a/flapi/_consts.py b/flapi/_consts.py index 2c967f5..77453bc 100644 --- a/flapi/_consts.py +++ b/flapi/_consts.py @@ -113,6 +113,18 @@ certain exit codes could break the MIDI spec) """ +MSG_TYPE_NAMES = { + MSG_TYPE_HEARTBEAT: "heartbeat", + MSG_TYPE_VERSION_QUERY: "version query", + MSG_TYPE_EXEC: "exec", + MSG_TYPE_EVAL: "eval", + MSG_TYPE_STDOUT: "stdout", + MSG_TYPE_EXIT: "exit", +} +""" +Names of message types +""" + MSG_STATUS_OK = 0x00 """ Message was processed correctly. diff --git a/flapi/cli/consts.py b/flapi/cli/consts.py index 04ebba7..cdc9870 100644 --- a/flapi/cli/consts.py +++ b/flapi/cli/consts.py @@ -14,3 +14,6 @@ CONNECTION_TIMEOUT = 60.0 +""" +The maximum duration to wait for a connection with FL Studio +""" diff --git a/flapi/cli/repl.py b/flapi/cli/repl.py index ae835d9..caaca75 100644 --- a/flapi/cli/repl.py +++ b/flapi/cli/repl.py @@ -23,6 +23,7 @@ ) from flapi import _consts as consts from flapi.cli import consts as cli_consts +from .util import handle_verbose try: import IPython from IPython import start_ipython @@ -196,12 +197,15 @@ def start_ipython_shell(): help="Maximum time to wait to establish a connection with FL Studio", default=cli_consts.CONNECTION_TIMEOUT, ) +@click.option('-v', '--verbose', count=True) def repl( shell: Optional[str] = None, port: str = consts.DEFAULT_PORT_NAME, timeout: float = cli_consts.CONNECTION_TIMEOUT, + verbose: int = 0, ): """Main function to set up the Python shell""" + handle_verbose(verbose) print("Flapi interactive shell") print(f"Client version: {flapi.__version__}") print(f"Python version: {sys.version}") diff --git a/flapi/cli/uninstall.py b/flapi/cli/uninstall.py index cf59cd7..3bffe56 100644 --- a/flapi/cli/uninstall.py +++ b/flapi/cli/uninstall.py @@ -19,27 +19,16 @@ prompt=True, help="The path of the Image-Line data directory. Set to '-' for default", ) -@click.option( - "-y", - "--yes", - is_flag=True, - help="Proceed with uninstallation without confirmation", - prompt="Are you sure you want to uninstall the Flapi server?" +@click.confirmation_option( + prompt="Are you sure you want to uninstall the Flapi server?", ) -def uninstall( - data_dir: Path, - yes: bool = False, -): +def uninstall(data_dir: Path): """ Uninstall the Flapi server """ # Determine scripts folder location server_location = output_dir(data_dir) - if not yes: - print("Operation cancelled") - exit(1) - # Remove it rmtree(server_location) print("Success!") diff --git a/flapi/cli/util.py b/flapi/cli/util.py index a03de6a..a656b0f 100644 --- a/flapi/cli/util.py +++ b/flapi/cli/util.py @@ -5,6 +5,16 @@ """ from typing import Optional from pathlib import Path +import logging + + +def handle_verbose(verbose: int): + if verbose == 0: + return + elif verbose == 1: + logging.basicConfig(level="INFO") + else: + logging.basicConfig(level="DEBUG") def yn_prompt(prompt: str, default: Optional[bool] = None) -> bool: