diff --git a/core/remote_file.py b/core/remote_file.py index 02faf765fa..af255f8adf 100755 --- a/core/remote_file.py +++ b/core/remote_file.py @@ -27,6 +27,7 @@ import traceback import time from core.utils import * +from contextlib import contextmanager class SendMessageException(Exception): @@ -360,8 +361,9 @@ def send_message(self, message): class FileSyncServer(RemoteFileServer): def __init__(self, host, port): - self.file_dict = {} super().__init__(host, port) + self.file_dict = {} + self.file_locks = {} def handle_message(self, message): command = message["command"] @@ -379,26 +381,49 @@ def handle_message(self, message): elif command == "update_file": return self.handle_update_file(message) + @contextmanager + def file_access_lock(self, path): + path = os.path.abspath(os.path.expanduser(path)) + + # even though messages arrive in sequence, there are edge cases: + # - change_file message comes before handle_open_file finishes. + # - utils.get_buffer_content utils.get_file_content_from_file_server + # try to read file content before handle_open_file finishes. + # + # Add a threading.Lock() for each file operation to ensure atomic access. + lock = self.file_locks.setdefault(path, threading.Lock()) + try: + lock.acquire() + yield + finally: + lock.release() + + def get_file_content(self, path): + with self.file_access_lock(path): + return self.file_dict.get(path, "") + def handle_update_file(self, message): - path = os.path.expanduser(message["path"]) - self.file_dict[path] = message["content"] + path = message["path"] + with self.file_access_lock(path): + self.file_dict[path] = message["content"] def handle_remote_sync(self, message): remote_info = message["remote_connection_info"] set_remote_connection_info(remote_info) def handle_open_file(self, message): - path = os.path.expanduser(message["path"]) + path = message["path"] response = {**message, "path": path} if os.path.exists(path): - with open(path) as f: - content = f.read() - response.update({ - "path": path, - "content": content - }) - self.file_dict[path] = content + with self.file_access_lock(path): + with open(path) as f: + content = f.read() + response.update({ + "path": path, + "content": content + }) + self.file_dict[path] = content else: response.update({ "path": path, @@ -410,24 +435,28 @@ def handle_open_file(self, message): def handle_change_file(self, message): path = message["path"] - if path not in self.file_dict: - with open(path) as f: - self.file_dict[path] = f.read() - self.file_dict[path] = rebuild_content_from_diff(self.file_dict[path], message["args"][0], message["args"][1], message["args"][3]) + with self.file_access_lock(path): + if path not in self.file_dict: + with open(path) as f: + self.file_dict[path] = f.read() + + self.file_dict[path] = rebuild_content_from_diff(self.file_dict[path], message["args"][0], message["args"][1], message["args"][3]) def handle_save_file(self, message): path = message["path"] if path in self.file_dict: - with open(path, 'w') as file: - file.write(self.file_dict[path]) + with self.file_access_lock(path): + with open(path, 'w') as file: + file.write(self.file_dict[path]) def handle_close_file(self, message): path = message["path"] if path in self.file_dict: - del self.file_dict[path] + with self.file_access_lock(path): + del self.file_dict[path] def close_all_files(self): self.file_dict.clear() diff --git a/core/utils.py b/core/utils.py index 891aaac466..03ecd41d8f 100755 --- a/core/utils.py +++ b/core/utils.py @@ -126,15 +126,15 @@ def get_buffer_content(filename, buffer_name): global lsp_bridge_server if lsp_bridge_server and lsp_bridge_server.file_server: - return lsp_bridge_server.file_server.file_dict[filename] + return lsp_bridge_server.file_server.get_file_content(filename) else: return get_emacs_func_result('get-buffer-content', buffer_name) def get_file_content_from_file_server(filename): global lsp_bridge_server - if lsp_bridge_server and lsp_bridge_server.file_server and filename in lsp_bridge_server.file_server.file_dict: - return lsp_bridge_server.file_server.file_dict[filename] + if lsp_bridge_server and lsp_bridge_server.file_server: + return lsp_bridge_server.file_server.get_file_content(filename) else: return ""