From 519ff1a72a28ead5121c48c1ad6dfd9e80b2b7c7 Mon Sep 17 00:00:00 2001 From: Xida Ren Date: Thu, 27 Jun 2024 18:30:57 -0700 Subject: [PATCH] new arch --- uniclip.lua | 68 +++++++++++++++++++++++++++++------------ uniclip/client.py | 78 ++++++++++++++++++++++++++++------------------- uniclip/server.py | 65 ++++++++++++++++----------------------- 3 files changed, 122 insertions(+), 89 deletions(-) diff --git a/uniclip.lua b/uniclip.lua index 1479c0d..84022ba 100644 --- a/uniclip.lua +++ b/uniclip.lua @@ -54,8 +54,18 @@ local function make_request(method, url, body) return result end +-- Hash content +local function hash_content(content) + return vim.fn.sha256(content) +end + +-- Get current timestamp +local function get_timestamp() + return os.time() +end + -- Send content to server -function M.send_to_server(content) +function M.send_to_server(content, timestamp) local group_id, server_address = load_config() local temp_file = os.tmpname() @@ -64,7 +74,12 @@ function M.send_to_server(content) log_error("Failed to create temporary file") return end - f:write(content) + f:write(vim.fn.json_encode({ + group_id = group_id, + client_id = "neovim-client", + content = content, + timestamp = timestamp + })) f:close() local curl_command = string.format( @@ -88,34 +103,40 @@ function M.send_to_server(content) end -- Receive content from server -function M.receive_from_server() +function M.receive_from_server(content_hash, timestamp) local group_id, server_address = load_config() - local response = make_request("GET", server_address .. "/poll/" .. group_id .. "/neovim-client") + local url = string.format("%s/poll/%s/neovim-client?hash=%s×tamp=%d", server_address, group_id, content_hash, timestamp) + local response = make_request("GET", url) local success, data = pcall(vim.fn.json_decode, response) if not success then log_error("Failed to decode server response: " .. response) return nil end - return data and data.content + return data end -- Clipboard sync functions local last_clipboard = "" +local last_timestamp = 0 local current_backoff = poll_interval -- Update clipboard with exponential backoff local function update_clipboard() - local content = M.receive_from_server() - if content then - if content ~= last_clipboard then - vim.fn.setreg('"', content) - last_clipboard = content + local content_hash = hash_content(last_clipboard) + local data = M.receive_from_server(content_hash, last_timestamp) + if data then + if data.status == "update_needed" then + if data.content ~= last_clipboard then + vim.fn.setreg('"', data.content) + last_clipboard = data.content + last_timestamp = data.timestamp + end end current_backoff = poll_interval -- Reset backoff on successful poll else log_error("Failed to receive update from server. Retrying in " .. current_backoff / 1000 .. " seconds.") - current_backoff = math.min(current_backoff * 4, max_backoff) -- Exponential backoff with a factor of 4 + current_backoff = math.min(current_backoff * 2, max_backoff) -- Exponential backoff end end @@ -136,16 +157,20 @@ function M.setup() group = vim.api.nvim_create_augroup("Uniclip", { clear = true }), callback = function() local yanked_text = table.concat(vim.v.event.regcontents, "\n") - M.send_to_server(yanked_text) + local timestamp = get_timestamp() + M.send_to_server(yanked_text, timestamp) last_clipboard = yanked_text + last_timestamp = timestamp end, }) vim.keymap.set('n', 'p', function() - local content = M.receive_from_server() - if content and content ~= last_clipboard then - vim.fn.setreg('"', content) - last_clipboard = content + local content_hash = hash_content(last_clipboard) + local data = M.receive_from_server(content_hash, last_timestamp) + if data and data.status == "update_needed" and data.content ~= last_clipboard then + vim.fn.setreg('"', data.content) + last_clipboard = data.content + last_timestamp = data.timestamp end return 'p' end, { expr = true }) @@ -156,19 +181,24 @@ end -- Function to check Uniclip status function M.check_status() local group_id, server_address = load_config() - local response = make_request("GET", server_address .. "/poll/" .. group_id .. "/neovim-client") + local content_hash = hash_content(last_clipboard) + local response = make_request("GET", string.format("%s/poll/%s/neovim-client?hash=%s×tamp=%d", server_address, group_id, content_hash, last_timestamp)) local success, data = pcall(vim.fn.json_decode, response) if success and data then print("Uniclip server is running and reachable.") print("Server address: " .. server_address) print("Group ID: " .. group_id) print("Current backoff: " .. current_backoff / 1000 .. " seconds") - print("Last received content: " .. vim.inspect(data.content)) + print("Last clipboard content hash: " .. content_hash) + print("Last clipboard timestamp: " .. os.date("%Y-%m-%d %H:%M:%S", last_timestamp)) + print("Server response: " .. vim.inspect(data)) else print("Unable to reach Uniclip server or server is not responding correctly.") print("Server address: " .. server_address) print("Group ID: " .. group_id) print("Current backoff: " .. current_backoff / 1000 .. " seconds") + print("Last clipboard content hash: " .. content_hash) + print("Last clipboard timestamp: " .. os.date("%Y-%m-%d %H:%M:%S", last_timestamp)) print("Server response: " .. vim.inspect(response)) end end @@ -210,4 +240,4 @@ end, { end, }) -return M +return M \ No newline at end of file diff --git a/uniclip/client.py b/uniclip/client.py index 56aff76..193f4a0 100644 --- a/uniclip/client.py +++ b/uniclip/client.py @@ -5,8 +5,10 @@ import pyperclip import socket import uuid +import hashlib + class Client: - # Change: Added force_headless parameter to __init__ + # Added timestamp to __init__ def __init__(self, group_id, server_address, logger, force_headless=False): self.group_id = group_id self.server_address = server_address @@ -16,6 +18,8 @@ def __init__(self, group_id, server_address, logger, force_headless=False): self.force_headless = force_headless self.headless = self._detect_headless() self.client_id = self._generate_client_id() + self.last_clipboard = '' + self.last_timestamp = 0 self.logger.debug(f"Client initialized with group_id: {group_id}, server_address: {server_address}, client_id: {self.client_id}, force_headless: {force_headless}") def _generate_client_id(self): @@ -24,7 +28,6 @@ def _generate_client_id(self): return f"{hostname}-{short_uuid}" def _detect_headless(self): - # Change: Use force_headless if set if self.force_headless: self.logger.info("Forced headless mode") return True @@ -46,7 +49,6 @@ def run(self): threading.Thread(target=self.monitor_clipboard, daemon=True).start() self.register_with_server() - # Keep the main thread running try: while self.running: time.sleep(1) @@ -54,6 +56,7 @@ def run(self): self.logger.info("Stopping client...") self.running = False + # Modified to use file's last modified time def monitor_clipboard_file(self): last_modified = 0 self.logger.debug(f"Starting to monitor clipboard file: {self.clipboard_file}") @@ -64,25 +67,29 @@ def monitor_clipboard_file(self): last_modified = current_modified with open(self.clipboard_file, 'r') as f: content = f.read() - self.logger.debug(f"Clipboard file changed. New content: {content[:50]}...") - self.send_to_server(content) + if content != self.last_clipboard: + self.logger.debug(f"Clipboard file changed. New content: {content[:50]}...") + self.last_clipboard = content + self.last_timestamp = int(current_modified) + self.send_to_server(content, self.last_timestamp) except FileNotFoundError: self.logger.debug(f"Clipboard file not found. Creating: {self.clipboard_file}") open(self.clipboard_file, 'a').close() except Exception as e: self.logger.error(f"Error monitoring clipboard file: {e}") - time.sleep(0.5) # Check file every 0.5 seconds + time.sleep(0.5) + # Modified to record timestamp when copying def monitor_clipboard(self): - last_clipboard = '' self.logger.debug("Starting to monitor system clipboard") while self.running: try: current_clipboard = pyperclip.paste() - if current_clipboard != last_clipboard: + if current_clipboard != self.last_clipboard: self.logger.debug(f"Clipboard content changed. New content: {current_clipboard[:50]}...") - last_clipboard = current_clipboard - self.send_to_server(current_clipboard) + self.last_clipboard = current_clipboard + self.last_timestamp = int(time.time()) + self.send_to_server(current_clipboard, self.last_timestamp) except pyperclip.PyperclipException as e: self.logger.error(f"Error accessing clipboard: {e}") self.logger.info("Switching to headless mode") @@ -90,7 +97,7 @@ def monitor_clipboard(self): self.logger.debug("Starting clipboard file monitoring thread") threading.Thread(target=self.monitor_clipboard_file, daemon=True).start() break - time.sleep(0.5) # Check clipboard every 0.5 seconds + time.sleep(0.5) def register_with_server(self): self.logger.debug(f"Attempting to register with server: {self.server_address}") @@ -108,45 +115,54 @@ def register_with_server(self): except requests.RequestException as e: self.logger.error(f"Error connecting to server: {e}") + # Modified to include content hash and timestamp def poll_server(self): self.logger.debug("Starting to poll server for updates") while self.running: try: + content_hash = hashlib.md5(self.last_clipboard.encode()).hexdigest() self.logger.debug(f"Polling server: {self.server_address}/poll/{self.group_id}/{self.client_id}") - response = requests.get(f"{self.server_address}/poll/{self.group_id}/{self.client_id}") + response = requests.get(f"{self.server_address}/poll/{self.group_id}/{self.client_id}", + params={"hash": content_hash, "timestamp": self.last_timestamp}) if response.status_code == 200: data = response.json() - clipboard_content = data.get('content') - if clipboard_content: - self.logger.debug(f"Received new content from server: {clipboard_content[:50]}...") - if self.headless: - self.update_clipboard_file(clipboard_content) - else: - try: - pyperclip.copy(clipboard_content) - self.logger.debug("Successfully copied new content to clipboard") - except pyperclip.PyperclipException as e: - self.logger.error(f"Error copying to clipboard: {e}") - self.logger.info("Switching to headless mode") - self.headless = True + if data.get('status') == 'update_needed': + clipboard_content = data.get('content') + timestamp = data.get('timestamp') + if clipboard_content and timestamp: + self.logger.debug(f"Received new content from server: {clipboard_content[:50]}...") + if self.headless: self.update_clipboard_file(clipboard_content) - self.logger.info("Received new clipboard content from server") + else: + try: + pyperclip.copy(clipboard_content) + self.logger.debug("Successfully copied new content to clipboard") + except pyperclip.PyperclipException as e: + self.logger.error(f"Error copying to clipboard: {e}") + self.logger.info("Switching to headless mode") + self.headless = True + self.update_clipboard_file(clipboard_content) + self.last_clipboard = clipboard_content + self.last_timestamp = timestamp + self.logger.info("Received new clipboard content from server") else: self.logger.debug("No new content received from server") else: self.logger.warning(f"Unexpected status code from server: {response.status_code}") - time.sleep(0.5) # Poll every 0.5 seconds + time.sleep(0.5) except requests.RequestException as e: self.logger.error(f"Error polling server: {e}") - time.sleep(5) # Wait 5 seconds before retrying + time.sleep(5) - def send_to_server(self, content): + # Modified to include timestamp + def send_to_server(self, content, timestamp): self.logger.debug(f"Sending update to server: {content[:50]}...") try: response = requests.post(f"{self.server_address}/update", json={ "group_id": self.group_id, "client_id": self.client_id, - "content": content + "content": content, + "timestamp": timestamp }) if response.status_code == 200: self.logger.info(f"Sent update to server: {content[:20]}...") @@ -162,4 +178,4 @@ def update_clipboard_file(self, content): f.write(content) self.logger.debug("Clipboard file updated successfully") except Exception as e: - self.logger.error(f"Error updating clipboard file: {e}") + self.logger.error(f"Error updating clipboard file: {e}") \ No newline at end of file diff --git a/uniclip/server.py b/uniclip/server.py index 2f0a45b..eddd1bd 100644 --- a/uniclip/server.py +++ b/uniclip/server.py @@ -1,7 +1,9 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Query from pydantic import BaseModel from waitress import serve import uvicorn +import sqlite3 +from datetime import datetime class RegisterData(BaseModel): group_id: str @@ -11,9 +13,7 @@ class UpdateData(BaseModel): group_id: str client_id: str content: str - -import sqlite3 -from datetime import datetime + timestamp: int class DatabaseManager: def __init__(self, db_name='uniclip.db'): @@ -30,34 +30,32 @@ def init_db(self): group_id TEXT, content TEXT, client_id TEXT, - timestamp DATETIME + timestamp INTEGER ) ''') self.conn.commit() - def record_message(self, group_id, content, client_id): + def record_message(self, group_id, content, client_id, timestamp): self.cursor.execute(''' INSERT INTO messages (group_id, content, client_id, timestamp) VALUES (?, ?, ?, ?) - ''', (group_id, content, client_id, datetime.now())) + ''', (group_id, content, client_id, timestamp)) self.conn.commit() - def get_messages(self, group_id, limit=10): + def get_latest_message(self, group_id): self.cursor.execute(''' SELECT * FROM messages WHERE group_id = ? ORDER BY timestamp DESC - LIMIT ? - ''', (group_id, limit)) - return self.cursor.fetchall() - + LIMIT 1 + ''', (group_id,)) + return self.cursor.fetchone() class Server: def __init__(self, logger): self.logger = logger self.db_manager = DatabaseManager() self.clients = {} - self.pending_updates = {} self.app = FastAPI() self.setup_routes() self.logger.info("Server initialized") @@ -90,40 +88,29 @@ async def handle_update(self, data: UpdateData): group_id = data.group_id client_id = data.client_id content = data.content + timestamp = data.timestamp self.logger.debug(f"Received update request from {client_id} for group: {group_id}") - self.db_manager.record_message(group_id, content, client_id) + self.db_manager.record_message(group_id, content, client_id, timestamp) self.logger.info(f"Message received from {client_id} in group {group_id}") - self.logger.debug(f"Message content: {content[:50]}...") # Log first 50 characters of content - - if group_id in self.clients: - self.logger.debug(f"Processing update for clients in group {group_id}") - for client in self.clients[group_id]: - if client != client_id: - if group_id not in self.pending_updates: - self.pending_updates[group_id] = {} - self.pending_updates[group_id][client] = content - self.logger.debug(f"Queued update for client: {client}") - else: - self.logger.warning(f"Received update for non-existent group: {group_id}") + self.logger.debug(f"Message content: {content[:50]}... Timestamp: {timestamp}") - self.logger.debug(f"Current pending updates: {self.pending_updates}") return {"status": "updated"} - async def handle_poll(self, group_id: str, client_id: str): + async def handle_poll(self, group_id: str, client_id: str, hash: str = Query(...), timestamp: int = Query(...)): self.logger.debug(f"Received poll request from {client_id} for group: {group_id}") - if group_id in self.pending_updates and client_id in self.pending_updates[group_id]: - content = self.pending_updates[group_id].pop(client_id) - self.logger.info(f"Sending update to client {client_id} in group {group_id}") - self.logger.debug(f"Update content: {content[:50]}...") # Log first 50 characters of content - if not self.pending_updates[group_id]: - del self.pending_updates[group_id] - self.logger.debug(f"Removed empty pending updates for group: {group_id}") - return {"content": content} - else: - self.logger.debug(f"No pending updates for client {client_id} in group {group_id}") - return {"content": None} + latest_message = self.db_manager.get_latest_message(group_id) + + if latest_message: + _, _, content, _, server_timestamp = latest_message + if server_timestamp > timestamp: + self.logger.info(f"Sending update to client {client_id} in group {group_id}") + self.logger.debug(f"Update content: {content[:50]}... Timestamp: {server_timestamp}") + return {"status": "update_needed", "content": content, "timestamp": server_timestamp} + + self.logger.debug(f"No update needed for client {client_id} in group {group_id}") + return {"status": "no_update"} def create_server(logger): return Server(logger)