fix: properly handle the cleanup of workspaces when closing a window containing them. as well as tagging a window similarly to how we tag views that contain a buffer managed by codemp.

fix: TaskManager, properly implement the stopping logic: Tasks in asyncio when .cancel()'ed are not immediatelly stopped.
but only a request of cancellation is made. subsequently awaiting the task allows it to raise a cancelledError exception,
do any necessary cleanup (finally block).
chore: removed unused import in lib.

Former-commit-id: 04f294c50b180e3676fd026d9a47732cdf6511a6
This commit is contained in:
Camillo Schenone 2024-02-27 00:06:58 +01:00
parent 8a67c7ce93
commit a26a51cecf
5 changed files with 76 additions and 29 deletions

View file

@ -38,8 +38,6 @@ async def disconnect_client():
for vws in CLIENT.workspaces.values(): for vws in CLIENT.workspaces.values():
vws.cleanup() vws.cleanup()
CLIENT = None
def plugin_unloaded(): def plugin_unloaded():
global CLIENT global CLIENT
@ -51,10 +49,26 @@ def plugin_unloaded():
# Listeners # Listeners
############################################################################## ##############################################################################
class EventListener(sublime_plugin.EventListener): class EventListener(sublime_plugin.EventListener):
def on_exit(self) -> None: def on_exit(self):
global CLIENT global CLIENT
CLIENT.tm.release(True) CLIENT.tm.release(True)
def on_pre_close_window(self, window):
global CLIENT
s = window.settings()
if s.get(g.CODEMP_WINDOW_TAG, False):
for wsid in s[g.CODEMP_WINDOW_WORKSPACES]:
ws = CLIENT[wsid]
if ws is not None:
status_log(
f"current active: {CLIENT.active_workspace.id}, ws = {ws.id}"
)
if ws.id == CLIENT.active_workspace.id:
CLIENT.active_workspace = None
CLIENT.tm.stop(f"{g.CURCTL_TASK_PREFIX}-{ws.id}")
ws.cleanup()
del CLIENT.workspaces[wsid]
class CodempClientViewEventListener(sublime_plugin.ViewEventListener): class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
@classmethod @classmethod
@ -102,7 +116,7 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener):
vbuff = CLIENT[wsid].get_by_local(self.view.buffer_id()) vbuff = CLIENT[wsid].get_by_local(self.view.buffer_id())
vbuff.cleanup() vbuff.cleanup()
CLIENT.tm.stop_and_pop(f"{g.BUFFCTL_TASK_PREFIX}-{vbuff.codemp_id}") CLIENT.tm.stop(f"{g.BUFFCTL_TASK_PREFIX}-{vbuff.codemp_id}")
class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): class CodempClientTextChangeListener(sublime_plugin.TextChangeListener):
@ -283,6 +297,7 @@ class RawBufferId(sublime_plugin.TextInputHandler):
def placeholder(self): def placeholder(self):
return "Buffer Id" return "Buffer Id"
# Share Command # Share Command
# ############################################################################# # #############################################################################
# class CodempShareCommand(sublime_plugin.WindowCommand): # class CodempShareCommand(sublime_plugin.WindowCommand):

View file

@ -1,4 +1,5 @@
from typing import Optional from typing import Optional
import asyncio
import Codemp.ext.sublime_asyncio as rt import Codemp.ext.sublime_asyncio as rt
@ -16,17 +17,18 @@ class TaskManager:
def sync(self, coro): def sync(self, coro):
rt.sync(coro) rt.sync(coro)
def store(self, task): def remove_stopped(self):
self.tasks.append(task) self.tasks = list(filter(lambda T: not T.cancelled(), self.tasks))
def store_named(self, task, name=None): def store(self, task, name=None):
if name is not None:
task.set_name(name) task.set_name(name)
self.store(task) self.tasks.append(task)
self.remove_stopped()
def store_named_lambda(self, name): def store_named_lambda(self, name):
def _store(task): def _store(task):
task.set_name(name) self.store(task, name)
self.store(task)
return _store return _store
@ -44,20 +46,20 @@ class TaskManager:
return self.task.pop(idx) return self.task.pop(idx)
return None return None
async def _stop(self, task):
task.cancel() # cancelling a task, merely requests a cancellation.
try:
await task
except asyncio.CancelledError:
return
def stop(self, name): def stop(self, name):
t = self.get_task(name) t = self.get_task(name)
if t is not None: if t is not None:
t.cancel() rt.sync(
self._stop(t)
def stop_and_pop(self, name) -> Optional: ) # awaiting the task blocks until it actually is finished.
idx, task = next(
((i, t) for (i, t) in enumerate(self.tasks) if t.get_name() == name),
(None, None),
)
if idx is not None:
task.cancel()
return self.tasks.pop(idx)
def stop_all(self): def stop_all(self):
for task in self.tasks: for task in self.tasks:
task.cancel() rt.sync(self._stop(task))

View file

@ -73,7 +73,7 @@ class VirtualWorkspace:
# mapping remote ids -> local ids # mapping remote ids -> local ids
self.id_map: dict[str, str] = {} self.id_map: dict[str, str] = {}
self.active_buffers: dict[str, VirtualBuffer] = {} self.active_buffers: dict[str, VirtualBuffer] = {} # local_id -> VBuff
# initialise the virtual filesystem # initialise the virtual filesystem
tmpdir = tempfile.mkdtemp(prefix="codemp_") tmpdir = tempfile.mkdtemp(prefix="codemp_")
@ -90,6 +90,13 @@ class VirtualWorkspace:
) )
self.sublime_window.set_project_data(proj_data) self.sublime_window.set_project_data(proj_data)
s: dict = self.sublime_window.settings()
if s.get(g.CODEMP_WINDOW_TAG, False):
s[g.CODEMP_WINDOW_WORKSPACES].append(self.id)
else:
s[g.CODEMP_WINDOW_TAG] = True
s[g.CODEMP_WINDOW_WORKSPACES] = [self.id]
def add_buffer(self, remote_id: str, vbuff: VirtualBuffer): def add_buffer(self, remote_id: str, vbuff: VirtualBuffer):
self.id_map[remote_id] = vbuff.view.buffer_id() self.id_map[remote_id] = vbuff.view.buffer_id()
self.active_buffers[vbuff.view.buffer_id()] = vbuff self.active_buffers[vbuff.view.buffer_id()] = vbuff
@ -100,6 +107,8 @@ class VirtualWorkspace:
for vbuff in self.active_buffers.values(): for vbuff in self.active_buffers.values():
vbuff.view.close() vbuff.view.close()
self.active_buffers = {} # drop all buffers, let them be garbace collected (hopefully)
d = self.sublime_window.project_data() d = self.sublime_window.project_data()
newf = list( newf = list(
filter( filter(
@ -112,6 +121,10 @@ class VirtualWorkspace:
status_log(f"cleaning up virtual workspace '{self.id}'") status_log(f"cleaning up virtual workspace '{self.id}'")
shutil.rmtree(self.rootdir, ignore_errors=True) shutil.rmtree(self.rootdir, ignore_errors=True)
s = self.sublime_window.settings()
del s[g.CODEMP_WINDOW_TAG]
del s[g.CODEMP_WINDOW_WORKSPACES]
def get_by_local(self, local_id: str) -> Optional[VirtualBuffer]: def get_by_local(self, local_id: str) -> Optional[VirtualBuffer]:
return self.active_buffers.get(local_id) return self.active_buffers.get(local_id)
@ -171,7 +184,9 @@ class VirtualClient:
try: try:
await self.handle.connect(server_host) await self.handle.connect(server_host)
except Exception as e: except Exception as e:
sublime.error_message(f"Could not connect:\n Make sure the server is up.\nerror: {e}") sublime.error_message(
f"Could not connect:\n Make sure the server is up.\nerror: {e}"
)
return return
id = await self.handle.user_id() id = await self.handle.user_id()
@ -185,7 +200,9 @@ class VirtualClient:
await self.handle.login(user, password, workspace_id) await self.handle.login(user, password, workspace_id)
except Exception as e: except Exception as e:
status_log(f"Failed to login to workspace '{workspace_id}'.\nerror: {e}") status_log(f"Failed to login to workspace '{workspace_id}'.\nerror: {e}")
sublime.error_message(f"Failed to login to workspace '{workspace_id}'.\nerror: {e}") sublime.error_message(
f"Failed to login to workspace '{workspace_id}'.\nerror: {e}"
)
return return
try: try:
@ -193,7 +210,9 @@ class VirtualClient:
workspace_handle = await self.handle.join_workspace(workspace_id) workspace_handle = await self.handle.join_workspace(workspace_id)
except Exception as e: except Exception as e:
status_log(f"Could not join workspace '{workspace_id}'.\nerror: {e}") status_log(f"Could not join workspace '{workspace_id}'.\nerror: {e}")
sublime.error_message(f"Could not join workspace '{workspace_id}'.\nerror: {e}") sublime.error_message(
f"Could not join workspace '{workspace_id}'.\nerror: {e}"
)
return return
vws = VirtualWorkspace(self, workspace_id, workspace_handle) vws = VirtualWorkspace(self, workspace_id, workspace_handle)
@ -233,7 +252,10 @@ class VirtualClient:
except asyncio.CancelledError: except asyncio.CancelledError:
status_log(f"cursor worker for '{vws.id}' stopped...") status_log(f"cursor worker for '{vws.id}' stopped...")
return raise
except Exception as e:
status_log(f"cursor worker '{vws.id}' crashed:\n{e}")
raise
self.tm.dispatch( self.tm.dispatch(
move_cursor_task(virtual_workspace), move_cursor_task(virtual_workspace),
@ -280,7 +302,11 @@ class VirtualClient:
) )
except asyncio.CancelledError: except asyncio.CancelledError:
status_log("'{}' buffer worker stopped...".format(vb.codemp_id)) status_log(f"'{vb.codemp_id}' buffer worker stopped...")
raise
except Exception as e:
status_log(f"buffer worker '{vb.codemp_id}' crashed:\n{e}")
raise
self.tm.dispatch( self.tm.dispatch(
apply_buffer_change_task(vbuff), apply_buffer_change_task(vbuff),

View file

@ -1,9 +1,13 @@
BUFFCTL_TASK_PREFIX = "buffer-ctl" BUFFCTL_TASK_PREFIX = "buffer-ctl"
CURCTL_TASK_PREFIX = "cursor-ctl" CURCTL_TASK_PREFIX = "cursor-ctl"
CODEMP_BUFFER_TAG = "codemp-buffer" CODEMP_BUFFER_TAG = "codemp-buffer"
CODEMP_REMOTE_ID = "codemp-buffer-id" CODEMP_REMOTE_ID = "codemp-buffer-id"
CODEMP_WORKSPACE_ID = "codemp-workspace-id" CODEMP_WORKSPACE_ID = "codemp-workspace-id"
CODEMP_WINDOW_TAG = "codemp-window"
CODEMP_WINDOW_WORKSPACES = "codemp-workspaces"
WORKSPACE_FOLDER_PREFIX = "CODEMP::" WORKSPACE_FOLDER_PREFIX = "CODEMP::"
SUBLIME_REGIONS_PREFIX = "codemp-cursors" SUBLIME_REGIONS_PREFIX = "codemp-cursors"
SUBLIME_STATUS_ID = "z_codemp_buffer" SUBLIME_STATUS_ID = "z_codemp_buffer"

View file

@ -1,6 +1,6 @@
use codemp::proto::common::Identity; use codemp::proto::common::Identity;
use pyo3::types::PyList; use pyo3::types::PyList;
use std::{format, ops::Deref, sync::Arc}; use std::{format, sync::Arc};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use codemp::prelude::*; use codemp::prelude::*;