From 78fb1ce7fffcfc6f551432f14f09d3cf131666e2 Mon Sep 17 00:00:00 2001 From: "shubham.dogra" Date: Tue, 16 Sep 2025 12:53:47 +0530 Subject: [PATCH 1/2] feat: added support for active signal for background tasks --- README.md | 28 +++++++++++++++++-- modules/dap/configuration.py | 5 ++++ modules/output_panel_terminus.py | 2 +- modules/tasks.py | 46 +++++++++++++++++++++++++++++++- start.py | 3 --- 5 files changed, 77 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3e0e12d..cdb69e2 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,10 @@ Tasks are based on sublime build_systems with more integration so they can be us see https://www.sublimetext.com/docs/build_systems.html -Tasks are basically the same as sublime builds but there are a few additional parameters. -`name` which will show up in the debugger UI and be the name of the panel +Tasks are basically the same as sublime builds but there are a few additional parameters: +- `name` - The name that will show up in the debugger UI and be the name of the panel +- `ready_signal_pattern` - (Optional) A regex pattern to detect when a background task is ready. Useful for services that need to start before debugging (e.g., waiting for "Listening on port 5005") +- `ready_signal_timeout` - (Optional) Timeout in seconds to wait for the ready signal pattern. Defaults to 60 seconds ``` "debugger_tasks" : [ @@ -92,6 +94,28 @@ Tasks are basically the same as sublime builds but there are a few additional pa } ] ``` + +### Background Tasks with Ready Signal +For background tasks that need to signal when they're ready (like starting a server before debugging), you can use the `ready_signal_pattern` and `ready_signal_timeout` parameters: + +``` +"debugger_tasks" : [ + { + "name" : "run_server", + "shell_cmd" : "java -jar server.jar", + "background" : true, + "ready_signal_pattern" : "Listening for transport dt_socket at address: 5005", + "ready_signal_timeout" : 60.0 + } +] +``` + +When a task has a `ready_signal_pattern`: +- The debugger will wait for the pattern to appear in the task's output before proceeding +- If the pattern is found, debugging continues +- If the timeout is reached without finding the pattern, a timeout message is shown +- This ensures services are fully started before the debugger attempts to connect + - Tasks can be run with `Debugger: Run Tasks` - You can run tasks before and after debugging by adding `pre_debug_task` or `post_debug_task` to your configuration specifying the name of the task to run. diff --git a/modules/dap/configuration.py b/modules/dap/configuration.py index ac45c7c..dffa2d2 100644 --- a/modules/dap/configuration.py +++ b/modules/dap/configuration.py @@ -106,6 +106,11 @@ def __init__(self, task: Task, variables: ConfigurationVariables, json: dict[str self.background: bool = json.get('background', False) self.start_file_regex: str | None = json.get('start_file_regex') self.end_file_regex: str | None = json.get('end_file_regex') + + # Regex pattern to detect when the background task is ready/active + self.ready_signal_pattern: str | None = json.get('ready_signal_pattern') + # Timeout in seconds to wait for the ready signal + self.ready_signal_timeout: float = json.get('ready_signal_timeout', 60.0) self.depends_on = json.get('depends_on') self.depends_on_order = json.get('depends_on_sequence') diff --git a/modules/output_panel_terminus.py b/modules/output_panel_terminus.py index 59719c3..3e58227 100644 --- a/modules/output_panel_terminus.py +++ b/modules/output_panel_terminus.py @@ -40,7 +40,7 @@ def __init__(self, debugger: Debugger, task: dap.TaskExpanded, is_terminal: bool arguments = task.copy() # if we don't remove these additional arguments Default.exec.ExecCommand will be unhappy - for key in ['name', 'background', 'start_file_regex', 'end_file_regex', 'depends_on', 'depends_on_order']: + for key in ['name', 'background', 'start_file_regex', 'end_file_regex', 'depends_on', 'depends_on_order', 'ready_signal_pattern', 'ready_signal_timeout']: if key in arguments: del arguments[key] diff --git a/modules/tasks.py b/modules/tasks.py index ea94f64..1764b9d 100644 --- a/modules/tasks.py +++ b/modules/tasks.py @@ -1,6 +1,11 @@ from __future__ import annotations from typing import TYPE_CHECKING, Awaitable +import asyncio +import re +import time +import sublime + from . import core from . import dap from . import ui @@ -80,7 +85,18 @@ async def update_when_done(): update_when_done() - if not task.background: + # For background tasks with a ready signal pattern, wait for it before returning + if task.background and task.ready_signal_pattern is not None: + debugger.console.info(f"Waiting for '{task.name}' to be ready...") + try: + await self.wait_for_ready_signal_with_timeout(terminal, task.ready_signal_pattern, task.ready_signal_timeout, debugger) + debugger.console.info(f"Task '{task.name}' is ready") + except asyncio.TimeoutError: + debugger.console.info(f"Timeout waiting for task '{task.name}'") + except Exception as e: + debugger.console.error(f"Error waiting for task '{task.name}': {e}") + elif not task.background: + # For non-background tasks, wait for completion await terminal.wait() def on_options(self, task: TerminusOutputPanel): @@ -112,6 +128,34 @@ def cancel(self, task: TerminusOutputPanel): self.removed(task) task.dispose() + async def wait_for_ready_signal_with_timeout(self, terminal: TerminusOutputPanel, pattern: str, timeout: float, debugger: Debugger) -> None: + """ + Wait for a regex pattern to appear in the task output with a timeout. + This is useful for background tasks that need to signal when they're ready. + """ + compiled_pattern = re.compile(pattern) + check_count = 0 + start_time = time.time() + + while not terminal.is_finished(): + # Check if we've exceeded the timeout + elapsed = time.time() - start_time + if elapsed > timeout: + raise asyncio.TimeoutError(f"Timeout waiting for pattern '{pattern}'") + + # Get the current content of the output view + content = terminal.view.substr(sublime.Region(0, terminal.view.size())) + check_count += 1 + + # Check if the pattern matches + if compiled_pattern.search(content): + return + + await core.delay(0.1) + + # If task finished without matching the pattern + raise Exception(f"Task finished without matching pattern '{pattern}'") + def dispose(self): while self.tasks: task = self.tasks.pop() diff --git a/start.py b/start.py index 6745bac..f010974 100644 --- a/start.py +++ b/start.py @@ -8,9 +8,6 @@ import sublime import sublime_plugin -from .modules import asyncio - - if sublime.version() < '4000': raise Exception('Debugger only supports Sublime Text 4') From 10e66495a5546a8dbc44fc77e0eb028a98dabbf4 Mon Sep 17 00:00:00 2001 From: "shubham.dogra" Date: Tue, 23 Sep 2025 11:56:24 +0530 Subject: [PATCH 2/2] feat: cancelled background tasks as well when session terminates --- modules/debugger.py | 29 ++++++++++++++++++++++++++++- modules/output_panel_terminus.py | 10 +++++++++- modules/tasks.py | 6 +++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/modules/debugger.py b/modules/debugger.py index bd1aa4e..732a1e3 100644 --- a/modules/debugger.py +++ b/modules/debugger.py @@ -108,6 +108,7 @@ def __init__(self, window: sublime.Window) -> None: self.external_terminals: dict[dap.Session, list[ExternalTerminal]] = {} self.integrated_terminals: dict[dap.Session, list[TerminusOutputPanel]] = {} + self.session_tasks: dict[dap.Session, list[TerminusOutputPanel]] = {} # Track pre-launch tasks per session self.memory_views: list[MemoryView] = [] self.disassembly_view: DisassembleView | None = None @@ -381,7 +382,13 @@ def current_session(self, session: dap.Session): self.on_session_active(session) async def session_task_request(self, session: dap.Session, task: dap.TaskExpanded): - return await self.tasks.run(self, task) + terminal = await self.tasks.run(self, task) + + # Track background pre-launch tasks for cleanup when session stops + if task.background and terminal: + self.session_tasks.setdefault(session, []).append(terminal) + + return terminal async def session_terminal_request(self, session: dap.Session, request: dap.RunInTerminalRequestArguments) -> dap.RunInTerminalResponse: response = self._on_session_run_terminal_requested(session, request) @@ -414,6 +421,21 @@ def _on_session_output(self, session: dap.Session, event: dap.OutputEvent): self.console.program_output(session, event) def remove_session(self, session: dap.Session): + # Clean up background tasks associated with this session + if session in self.session_tasks: + tasks_to_clean = self.session_tasks[session] + cancelled_tasks = [] + + for task in tasks_to_clean: + if not task.is_finished(): + self.tasks.cancel(task) + cancelled_tasks.append(task.task.name) + + if cancelled_tasks: + self.console.info(f'Cancelled background tasks: {", ".join(cancelled_tasks)}') + + del self.session_tasks[session] + core.remove_and_dispose(self.memory_views, lambda view: view.session == session) session.dispose() @@ -635,6 +657,11 @@ def dispose_terminals(self, unused_only: bool = False): except KeyError: ... + try: + del self.session_tasks[session] + except KeyError: + ... + self.tasks.remove_finished() def is_open(self): diff --git a/modules/output_panel_terminus.py b/modules/output_panel_terminus.py index 3e58227..e6be145 100644 --- a/modules/output_panel_terminus.py +++ b/modules/output_panel_terminus.py @@ -146,7 +146,15 @@ async def wait(self): def is_finished(self): return self.view.settings().get('terminus_view.finished') or self.future.done() - def cancel(self): ... + def cancel(self): + """Actually cancel/kill the running background process""" + if not self.is_finished(): + # Use terminus_cancel_build to actually kill the running process + # (terminus_cancel only closes UI, doesn't kill the process) + self.view.run_command('terminus_cancel_build') + # Set as cancelled if the future isn't done yet + if not self.future.done(): + self.future.set_exception(core.CancelledError) def dispose(self): super().dispose() diff --git a/modules/tasks.py b/modules/tasks.py index 1764b9d..df95578 100644 --- a/modules/tasks.py +++ b/modules/tasks.py @@ -98,6 +98,9 @@ async def update_when_done(): elif not task.background: # For non-background tasks, wait for completion await terminal.wait() + + # Return the terminal so it can be tracked for cleanup + return terminal def on_options(self, task: TerminusOutputPanel): if task.is_finished(): @@ -124,7 +127,8 @@ def cancel(self, task: TerminusOutputPanel): except ValueError: return - # todo actually cancel... + # Actually cancel the running background process + task.cancel() self.removed(task) task.dispose()