diff --git a/jupyter_collaboration/handlers.py b/jupyter_collaboration/handlers.py index ce3424ec..403bfd5c 100644 --- a/jupyter_collaboration/handlers.py +++ b/jupyter_collaboration/handlers.py @@ -153,14 +153,25 @@ async def open(self, room_id): # Close the connection if the document session expired session_id = self.get_query_argument("sessionId", "") if SERVER_SESSION != session_id: - self.close(1003, f"Document session {session_id} expired") + self.close( + 1003, + f"Document session {session_id} expired. You need to reload this browser tab.", + ) # cancel the deletion of the room if it was scheduled if self.room.cleaner is not None: self.room.cleaner.cancel() - # Initialize the room - await self.room.initialize() + try: + # Initialize the room + await self.room.initialize() + except Exception as e: + _, _, file_id = decode_file_path(self._room_id) + file = self._file_loaders[file_id] + self.log.error(f"Error initializing: {file.path}\n{e!r}", exc_info=e) + self.close( + 1003, f"Error initializing: {file.path}. You need to close the document." + ) self._emit(LogLevel.INFO, "initialize", "New client connected.") diff --git a/jupyter_collaboration/loaders.py b/jupyter_collaboration/loaders.py index 03284060..eba3fc8a 100644 --- a/jupyter_collaboration/loaders.py +++ b/jupyter_collaboration/loaders.py @@ -14,9 +14,7 @@ from jupyter_server.utils import ensure_async from jupyter_server_fileid.manager import BaseFileIdManager - -class OutOfBandChanges(Exception): - pass +from .utils import OutOfBandChanges class FileLoader: @@ -137,6 +135,11 @@ async def save_content(self, model: dict[str, Any]) -> dict[str, Any]: """ async with self._lock: path = self.path + if model["type"] not in {"directory", "file", "notebook"}: + # fall back to file if unknown type, the content manager only knows + # how to handle these types + model["type"] = "file" + m = await ensure_async( self._contents_manager.get( path, format=model["format"], type=model["type"], content=False diff --git a/jupyter_collaboration/rooms.py b/jupyter_collaboration/rooms.py index e9aa47a5..32657a6e 100644 --- a/jupyter_collaboration/rooms.py +++ b/jupyter_collaboration/rooms.py @@ -12,8 +12,8 @@ from ypy_websocket.websocket_server import YRoom from ypy_websocket.ystore import BaseYStore, YDocNotFound -from .loaders import FileLoader, OutOfBandChanges -from .utils import JUPYTER_COLLABORATION_EVENTS_URI, LogLevel +from .loaders import FileLoader +from .utils import JUPYTER_COLLABORATION_EVENTS_URI, LogLevel, OutOfBandChanges YFILE = YDOCS["file"] @@ -94,6 +94,7 @@ async def initialize(self) -> None: return self.log.info("Initializing room %s", self._room_id) + model = await self._file.load_content(self._file_format, self._file_type, True) async with self._update_lock: @@ -187,11 +188,17 @@ async def _on_content_change(self, event: str, args: dict[str, Any]) -> None: args (dict): A dictionary with format, type, last_modified. """ if event == "metadata" and self._last_modified < args["last_modified"]: - model = await self._file.load_content(self._file_format, self._file_type, True) - self.log.info("Out-of-band changes. Overwriting the content in room %s", self._room_id) self._emit(LogLevel.INFO, "overwrite", "Out-of-band changes. Overwriting the room.") + try: + model = await self._file.load_content(self._file_format, self._file_type, True) + except Exception as e: + msg = f"Error loading content from file: {self._file.path}\n{e!r}" + self.log.error(msg, exc_info=e) + self._emit(LogLevel.ERROR, None, msg) + return None + async with self._update_lock: self._document.source = model["content"] self._last_modified = model["last_modified"] @@ -255,7 +262,14 @@ async def _maybe_save_document(self) -> None: except OutOfBandChanges: self.log.info("Out-of-band changes. Overwriting the content in room %s", self._room_id) - model = await self._file.load_content(self._file_format, self._file_type, True) + try: + model = await self._file.load_content(self._file_format, self._file_type, True) + except Exception as e: + msg = f"Error loading content from file: {self._file.path}\n{e!r}" + self.log.error(msg, exc_info=e) + self._emit(LogLevel.ERROR, None, msg) + return None + async with self._update_lock: self._document.source = model["content"] self._last_modified = model["last_modified"] @@ -263,6 +277,11 @@ async def _maybe_save_document(self) -> None: self._emit(LogLevel.INFO, "overwrite", "Out-of-band changes while saving.") + except Exception as e: + msg = f"Error saving file: {self._file.path}\n{e!r}" + self.log.error(msg, exc_info=e) + self._emit(LogLevel.ERROR, None, msg) + class TransientRoom(YRoom): """A Y room for sharing state (e.g. awareness).""" diff --git a/jupyter_collaboration/utils.py b/jupyter_collaboration/utils.py index 649eecd8..5362b474 100644 --- a/jupyter_collaboration/utils.py +++ b/jupyter_collaboration/utils.py @@ -17,6 +17,18 @@ class LogLevel(Enum): CRITICAL = "CRITICAL" +class OutOfBandChanges(Exception): + pass + + +class ReadError(Exception): + pass + + +class WriteError(Exception): + pass + + def decode_file_path(path: str) -> Tuple[str, str, str]: """ Decodes a file path. The file path is composed by the format, diff --git a/packages/docprovider/src/yprovider.ts b/packages/docprovider/src/yprovider.ts index 3cbf8121..4061ebeb 100644 --- a/packages/docprovider/src/yprovider.ts +++ b/packages/docprovider/src/yprovider.ts @@ -121,19 +121,9 @@ export class WebSocketProvider implements IDocumentProvider { if (event.code === 1003) { console.error('Document provider closed:', event.reason); - showErrorMessage( - this._trans.__('Session expired'), - this._trans.__( - 'The document session expired. You need to reload this browser tab.' - ), - [Dialog.okButton({ label: this._trans.__('Reload') })] - ) - .then((r: any) => { - if (r.button.accept) { - window.location.reload(); - } - }) - .catch(e => window.location.reload()); + showErrorMessage(this._trans.__('Document session error'), event.reason, [ + Dialog.okButton() + ]); // Dispose shared model immediately. Better break the document model, // than overriding data on disk. @@ -142,7 +132,6 @@ export class WebSocketProvider implements IDocumentProvider { }; private _onSync = (isSynced: boolean) => { - console.log(`_onSync ${isSynced}`); if (isSynced) { this._ready.resolve(); this._yWebsocketProvider?.off('sync', this._onSync);