From 6bf6a840220b942a7d26264127d9bc72b92ccea6 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Thu, 20 Feb 2025 17:00:10 -0700 Subject: [PATCH 1/3] Add functionality and documentation for debug mode --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + streamdeck/__main__.py | 18 ++++++++++++-- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c781d2..a1b3de8 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ The following commands are required, which are the same as the Stream Deck softw - `-info`: Additional information (formatted as json) about the plugin environment, as provided by the Stream Deck software. -There are also two additional options for specifying action scripts to load. Note that you can't use both of these options together, and the Stream Deck software doesn't pass in these options. +There are also two additional options for specifying action scripts to load. Note that you can't use both of these options together, and the Stream Deck software doesn't pass in these options. - Plugin Directory: Pass the directory containing the plugin files as a positional argument: @@ -172,6 +172,58 @@ There are also two additional options for specifying action scripts to load. No streamdeck --action-scripts actions1.py actions2.py ``` +In addition to these, there is an additional option to use **debug mode**, which is discussed below. + +#### Debugging + +The SDK supports remote debugging capabilities, allowing you to attach a debugger after the plugin has started. This is particularly useful since Stream Deck plugins run as separate processes. + +To enable debug mode, pass in the option `--debug {debug port number}`, which tells the plugin to wait for a debugger to attach on that port number. + +```bash +streamdeck --debug 5675 +``` + +When running in debug mode, the plugin will pause for 10 seconds after initialization, giving you time to attach your debugger. You'll see a message in the logs indicating that the plugin is waiting for a debugger to attach. + +NOTE: If things get messy, and you have a prior instance already listening to that port, you should kill the process with something like the following command: + +```bash +kill $(lsof -t -i:$DEBUG_PORT) +``` + +#### Debugging with VS Code + +1. Create a launch configuration in `.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Attach to Stream Deck Plugin", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + } + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ], + "justMyCode": false, + } + ] +} +``` + +2. Start your plugin with debugging enabled +3. When you see the "waiting for debugger" message, use VS Code's Run and Debug view to attach using the configuration above +4. Set breakpoints and debug as normal + #### Configuration diff --git a/pyproject.toml b/pyproject.toml index 53e6b07..3f58275 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ "pytest-mock >= 3.14.0", "pytest-sugar >= 1.0.0", "tox >= 4.23.2", + "debugpy", ] [project.urls] diff --git a/streamdeck/__main__.py b/streamdeck/__main__.py index 8f7ed56..8368bbb 100644 --- a/streamdeck/__main__.py +++ b/streamdeck/__main__.py @@ -2,7 +2,7 @@ import logging import sys from pathlib import Path -from typing import Annotated, Union +from typing import Annotated, Optional import typer @@ -18,6 +18,16 @@ plugin = typer.Typer() +def setup_debug_mode(debug_port: int) -> None: + """Setup the debug mode for the plugin and wait for the debugger to attach.""" + import debugpy + + debugpy.listen(debug_port) + logger.info("Starting in debug mode. Waiting for debugger to attach on port %d...", debug_port) + debugpy.wait_for_client() + logger.info("Debugger attached.") + + @plugin.command() def main( port: Annotated[int, typer.Option("-p", "-port")], @@ -25,7 +35,8 @@ def main( register_event: Annotated[str, typer.Option("-registerEvent")], info: Annotated[str, typer.Option("-info")], plugin_dir: Annotated[Path, typer.Option(file_okay=False, exists=True, readable=True)] = Path.cwd(), # noqa: B008 - action_scripts: Union[list[str], None] = None, # noqa: UP007 + action_scripts: Optional[list[str]] = None, # noqa: UP007 + debug_port: Annotated[Optional[int], typer.Option("--debug", "-d")] = None, # noqa: UP007 ) -> None: """Start the Stream Deck plugin with the given configuration. @@ -43,6 +54,9 @@ def main( # a child logger with `logging.getLogger("streamdeck.mycomponent")`, all with the same handler/formatter configuration. configure_streamdeck_logger(name="streamdeck", plugin_uuid=plugin_uuid) + if debug_port: + setup_debug_mode(debug_port) + pyproject = PyProjectConfigs.validate_from_toml_file(plugin_dir / "pyproject.toml", action_scripts=action_scripts) actions = pyproject.streamdeck_plugin_actions From f564eb0435fa210620604171c2d7ebebfd79c415 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Tue, 25 Feb 2025 21:17:50 -0600 Subject: [PATCH 2/3] Add exception-handling and additional logging for the connection port --- streamdeck/__main__.py | 2 ++ streamdeck/websocket.py | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/streamdeck/__main__.py b/streamdeck/__main__.py index 8368bbb..c9c5a9d 100644 --- a/streamdeck/__main__.py +++ b/streamdeck/__main__.py @@ -54,6 +54,8 @@ def main( # a child logger with `logging.getLogger("streamdeck.mycomponent")`, all with the same handler/formatter configuration. configure_streamdeck_logger(name="streamdeck", plugin_uuid=plugin_uuid) + logger.info("Stream Deck listening to plugin UUID '%s' on port %d", plugin_uuid, port) + if debug_port: setup_debug_mode(debug_port) diff --git a/streamdeck/websocket.py b/streamdeck/websocket.py index 46927dd..8651837 100644 --- a/streamdeck/websocket.py +++ b/streamdeck/websocket.py @@ -59,8 +59,9 @@ def listen(self) -> Generator[str | bytes, Any, None]: message: str | bytes = self._client.recv() yield message - except ConnectionClosedOK: + except ConnectionClosedOK as exc: logger.debug("Connection was closed normally, stopping the client.") + logger.exception(dir(exc)) except ConnectionClosed: logger.exception("Connection was closed with an error.") @@ -70,7 +71,11 @@ def listen(self) -> Generator[str | bytes, Any, None]: def start(self) -> None: """Start the connection to the websocket server.""" - self._client = connect(uri=f"ws://localhost:{self._port}") + try: + self._client = connect(uri=f"ws://localhost:{self._port}") + except ConnectionRefusedError: + logger.exception("Failed to connect to the WebSocket server. Make sure the Stream Deck software is running.") + raise def stop(self) -> None: """Close the WebSocket connection, if open.""" From e8b06dc6223265a3d2e78232e2eb3d11945ba90d Mon Sep 17 00:00:00 2001 From: strohganoff Date: Tue, 25 Feb 2025 21:30:45 -0600 Subject: [PATCH 3/3] Fix console script entrypoint object to Typer command --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3f58275..f5cafd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ "Issues" = "https://github.com/strohganoff/python-streamdeck-plugin-sdk/issues" [project.scripts] - streamdeck = "streamdeck.__main__:main" + streamdeck = "streamdeck.__main__:plugin" [tool.setuptools.packages.find] include = ["streamdeck*"]