import asyncio import json import os import secrets import signal import subprocess import websockets import shutil from connect4 import PLAYER1, PLAYER2, Connect4 from user import User import requests from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class FileHandler(FileSystemEventHandler): def __init__(self, websocket,connected): super().__init__() self.websocket = websocket self.connected = connected def on_modified(self, event): if event.is_directory: return with open(event.src_path, 'r', encoding='utf-8') as file: content = file.read() # print(f'File {event.src_path} has been modified.') new_event = { "type": "file-modifie", "content": content, "path": event.src_path, } websockets.broadcast(self.connected,json.dumps(new_event)) # websockets.broadcast(self.connected,f'File {event.src_path} has been modified.') def on_created(self, event): if event.is_directory: new_event = { "type": "folder-create", "path": event.src_path, "name": event.src_path.split('/')[-1] } # print(new_event) websockets.broadcast(self.connected,json.dumps(new_event)) else: with open(event.src_path, 'r', encoding='utf-8') as file: content = file.read() # print(f'File {event.src_path} has been created.') new_event = { "type": "file-create", "content": content, "path": event.src_path, "name": event.src_path.split('/')[-1] } # print(new_event) websockets.broadcast(self.connected,json.dumps(new_event)) # websockets.broadcast(self.connected,f'File {event.src_path} has been created.') def on_deleted(self, event): if event.is_directory: new_event = { "type": "delete", "path": event.src_path, "name": event.src_path.split('/')[-1], } websockets.broadcast(self.connected,json.dumps(new_event)) else: # print(f'File {event.src_path} has been deleted.') new_event = { "type": "delete", "name": event.src_path.split('/')[-1], "path": event.src_path, } websockets.broadcast(self.connected,json.dumps(new_event)) def on_moved(self, event): if event.is_directory: new_event = { "type": "rename", "OldPath": event.src_path, "oldname": event.src_path.split('/')[-1], "NewPath": event.dest_path, "newname": event.dest_path.split('/')[-1], } websockets.broadcast(self.connected,json.dumps(new_event)) else: # print(f'File {event.src_path} has been renamed to {event.dest_path}.') new_event = { "type": "rename", "OldPath": event.src_path, "oldname": event.src_path.split('/')[-1], "NewPath": event.dest_path, "newname": event.dest_path.split('/')[-1], } websockets.broadcast(self.connected,json.dumps(new_event)) JOIN = {} WATCH = {} async def generate_file_structure(path, encoding='utf-8'): file_structure = {'name': os.path.basename(path), 'type': 'folder', 'path': path, 'children': []} try: entries = os.listdir(path) except FileNotFoundError: return file_structure # Return an empty structure for non-existing directories for entry in entries: entry_path = os.path.join(path, entry) if os.path.isdir(entry_path): child_structure =await generate_file_structure(entry_path, encoding) file_structure['children'].append(child_structure) elif os.path.isfile(entry_path): try: with open(entry_path, 'r', encoding=encoding) as file: content = file.read() except UnicodeDecodeError: content = 'Unable to read content' file_structure['children'].append({'name': entry, 'type': 'file', 'path': entry_path, 'content': content}) return file_structure async def rename_item(websocket, key,project_name, path,rpath,new_name,root, connected): old_path = os.path.join(os.getcwd(),'projects', key,project_name, rpath) new_name = new_name try: if os.path.exists(old_path): # Determine the new path new_path = os.path.join(os.path.dirname(old_path), new_name) # Rename the file or folder os.rename(old_path, new_path) websockets.broadcast(connected,'success') event = { "type": "rename-success", "data": f'{old_path}-->{new_path}', "path": path, "new_rpath":rpath, "name": new_name, "root":root, } websockets.broadcast(connected,json.dumps(event)) else: event = { "type": "rename-failed", "data": f'{old_path}-->{new_path} failed Item not found', "old_path": path, "new_name": new_name, } websockets.broadcast(connected,json.dumps(event)) except Exception as e: websockets.broadcast(connected,str(e)) async def delete_item(websocket, key,project_name, path,rpath,targetElementData, connected): try: item_path = os.path.join(os.getcwd(), 'projects', key,project_name, rpath) if os.path.exists(item_path): if os.path.isdir(item_path): shutil.rmtree(item_path) # Remove the directory and its contents elif os.path.isfile(item_path): os.remove(item_path) # Remove the file event = { "type": "delete-success", "data": path, "path":path, "targetElementData":targetElementData, } # print(event) websockets.broadcast(connected,json.dumps(event)) # websockets.broadcast(connected,'success') else: event = { "type": "delete-failed", "data": f'{item_path} Item not found', } websockets.broadcast(connected,json.dumps(event)) except Exception as e: # print(e) websockets.broadcast(connected,str(e)) async def get_file_content(websocket, key,project_name, path,rpath,name,connected): file_path = os.path.join(os.getcwd(), 'projects', key,project_name, rpath) try: with open(file_path, 'r', encoding='utf-8') as file: content = file.read() event = { "type": "content", "content": content, 'fileName':name, 'rfilePath':rpath, 'filePath':path, } await websocket.send(json.dumps(event)) except Exception as e: event = { "type": "error", "message": f"Failed to read file content: {str(e)}", } await websocket.send(json.dumps(event)) async def create_file(websocket, key,project_name, path,name,root,targetElementData,rpath, connected): file_path = os.path.join(os.getcwd(), 'projects', key,project_name, rpath,name) # Create the file with open(file_path, 'w'): pass event = { "type": "file-created", "data": file_path, "path": path, "name": name, "root":root, "targetElementData":targetElementData, } websockets.broadcast(connected,json.dumps(event)) async def create_folder(websocket, key,project_name, path,name,root,targetElementData,rpath, connected): folder_path = os.path.join(os.getcwd(), 'projects', key,project_name, rpath,name) # Create the folder os.makedirs(folder_path) event = { "type": "folder-created", "data": folder_path, "path": path, "name": name, "root":root, "targetElementData":targetElementData, } # print(folder_path,'created') websockets.broadcast(connected,json.dumps(event)) async def wirte_file(websocket, key,project_name, path, content, connected): try: file_path = os.path.join(os.getcwd(), 'projects', key,project_name, path) file_content = content with open(file_path, 'w', encoding='utf-8') as file: file.write(file_content) event = { "type": "write-success", "data": file_path, "path": path, "content": content, } # websockets.broadcast(connected,json.dumps(event)) except FileNotFoundError as e: event = { "type": "write-error", "data": e, } websockets.broadcast(connected,json.dumps(event)) async def read_process_output(process, websocket): async for line in process.stdout: # print('sending line') event = { "type": "terminal-data", "data": line.strip().decode('utf-8'), } await websocket.send(json.dumps(event)) async for line in process.stderr: # print(f'error:{line.strip()}') event = { "type": "terminal-error", "data": line.strip().decode('utf-8'), } await websocket.send(json.dumps(event)) async def handle_user_input(websocket,key, process, connected): while True: try: await asyncio.sleep(0.1) if process.returncode is not None: break # message = await websocket.recv() async for message in websocket: # user_input = json.loads(message) # print(user_input) # print(f'Received user input: {user_input["command"]["command"]}') # process_input(user_input["command"]["command"], process) event = json.loads(message) assert event["type"] == "cmd" # command = event["command"] # print(f'Received user input: {event}') try: if event["command"]["type"]=="shell": await asyncio.sleep(0.1) if process.returncode is not None: base_path = os.path.join(os.getcwd(), 'projects', key,event["project_name"]) directory_path = base_path event_handler = FileHandler(websocket,connected) observer = Observer() observer.schedule(event_handler, path=directory_path, recursive=True) observer.start() try: # await execute_command(websocket, key,event["project_name"], event["command"]["command"], connected) # base_path = os.path.join(os.getcwd(), 'projects', key,event["project_name"]) print(base_path) mod_command = f'cd {base_path} && {event["command"]["command"]}' print(mod_command) try: process = await asyncio.create_subprocess_shell( mod_command, # cwd=base_path, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, # text=True, ) async def send_message(message): # print('sending msg') # await websocket.send(f'data: {message}') websockets.broadcast(connected,json.dumps(message) ) await asyncio.gather( handle_user_input(websocket,key, process, connected), read_process_output(process, websocket) ) return_code = await process.wait() if return_code == 0: # await send_message('Code executed successfully') pass else: # await send_message(f'error:Execution failed with return code {return_code}') pass except Exception as e: await error(websocket, str(e)) except KeyboardInterrupt: pass # Handle KeyboardInterrupt to gracefully stop the observer observer.stop() observer.join() else: process_input(event["command"]["command"], process) elif event["command"]["type"]=="write": await wirte_file(websocket, key,event["project_name"], event["path"], event["content"], connected) elif event["command"]["type"]=="curl": response = requests.get(event['url']) event = { "type": "web-data", "data": response.text, } await websocket.send(json.dumps(event)) elif event["command"]["type"]=="create": if event["item"]=="folder": await create_folder(websocket, key,event["project_name"], event["path"],event["name"],event['root'],event['targetElementData'],event["rpath"], connected) else: await create_file(websocket, key,event["project_name"], event["path"],event["name"],event['root'],event['targetElementData'],event["rpath"], connected) elif event["command"]["type"]=="delete": await delete_item(websocket, key,event["project_name"], event["path"],event['rpath'],event['targetElementData'], connected) elif event["command"]["type"]=="get_content": await get_file_content(websocket, key,event["project_name"],event["filePath"], event["rfilePath"],event["fileName"] ,connected) elif event["command"]["type"]=="rename": await rename_item(websocket, key,event["project_name"], event["path"],event['rpath'], event["name"], event["root"], connected) elif event["command"]["type"]=="join": await join(websocket, event["join"]) elif event["command"]["type"]=="sendDir": data=json.loads(event["file_structure"]) event = { "type": "createDir", "path": data, "root":event['root'], } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="createItemUI": event = { "type": "createItemUI", 'targetElementData':event['targetElementData'], 'data':event['data'] } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="renameItemInUI": event = { "type": "renameItemInUI", 'path':event['path'], 'new_path':event['new_path'], 'name':event['name'], 'new_rpath':event['new_rpath'], } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="createFolderUI": event = { "type": "createFolderUI", 'targetElementData':event['targetElementData'], 'data':event['data'] } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="removeItemFromUI": event = { "type": "removeItemFromUI", 'targetElementData':event['targetElementData'], 'path':event['path'] } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="project": base_path = os.path.join(os.getcwd(), 'projects',key, event["project_name"]) data=json.loads(event["file_structure"]) await create_file_structure(websocket,data, base_path=base_path) # await rename_item(websocket, key,event["project_name"], event["path"], event["name"], connected) elif event["command"]["type"]=="collabration": event = { "type": "collabration", 'name': event["name"], 'line': event["cursorPos-line"], 'ch': event["cursorPos-ch"], 'file':event["file"], 'content': event["content"], 'color':event["color"] } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="cursor": event = { "type": "cursor", 'offset': event["offset"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="select": event = { "type": "select", 'id': event["id"], 'startOffset': event["startOffset"], 'endOffset': event["endOffset"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="addselection": event = { "type": "addselection", 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="insert": event = { "type": "insert", 'index': event["index"], 'text': event["text"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="replace": event = { "type": "replace", 'index': event["index"], 'length': event["length"], 'text': event["text"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="edelelte": event = { "type": "edelelte", 'index': event["index"], 'length': event["length"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="keep-alive": event = { "type": "keep-alive", } websockets.broadcast(connected, json.dumps(event)) else: # First player starts a new game. pass except RuntimeError as exc: # Send an "error" event if the move was illegal. await error(websocket, str(exc)) continue break except websockets.ConnectionClosed: # print("WebSocket connection closed") break except Exception as e: # print(f"Error in input thread: {str(e)}") pass def process_input(user_input, process): if process: try: if user_input=='Ctrl+C': print('process stoping') process.send_signal(signal.SIGINT) else: process.stdin.write(user_input.encode('utf-8') + b'\n') #process.stdin.flush() except Exception as e: # print(f"Error writing to process stdin: {str(e)}") pass else: # print("No process available to write to.") pass async def create_file_structure(websocket, data, base_path='.'): if data['type'] == 'folder': folder_path = os.path.join(base_path, data['name']) os.makedirs(folder_path, exist_ok=True) for child in data['children']: await create_file_structure(websocket,child, base_path=folder_path) elif data['type'] == 'file': file_path = os.path.join(base_path, data['name']) with open(file_path, 'w', encoding='utf-8') as file: file.write(data['content']) event = { "type": "msg", "message": "project created", } await websocket.send(json.dumps(event)) async def error(websocket, message): """ Send an error message. """ event = { "type": "error", "message": message, } await websocket.send(json.dumps(event)) async def exe(websocket,connected,key): """ Receive and process moves from a player. """ # print('in exe') async for message in websocket: # Parse a "play" event from the UI. # print(message) event = json.loads(message) assert event["type"] == "cmd" # command = event["command"] # print(event) try: if event["command"]["type"]=="shell": base_path = os.path.join(os.getcwd(), 'projects', key,event["project_name"]) directory_path = base_path event_handler = FileHandler(websocket,connected) observer = Observer() observer.schedule(event_handler, path=directory_path, recursive=True) observer.start() try: # await execute_command(websocket, key,event["project_name"], event["command"]["command"], connected) # base_path = os.path.join(os.getcwd(), 'projects', key,event["project_name"]) print(base_path) mod_command = f'cd {base_path} && {event["command"]["command"]}' print(mod_command) try: process = await asyncio.create_subprocess_shell( mod_command, # cwd=base_path, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, # text=True, ) async def send_message(message): # print('sending msg') # await websocket.send(f'data: {message}') websockets.broadcast(connected,json.dumps(message) ) await asyncio.gather( handle_user_input(websocket,key, process, connected), read_process_output(process, websocket) ) return_code = await process.wait() if return_code == 0: # await send_message('Code executed successfully') pass else: # await send_message(f'error:Execution failed with return code {return_code}') pass except Exception as e: await error(websocket, str(e)) except KeyboardInterrupt: pass # Handle KeyboardInterrupt to gracefully stop the observer observer.stop() observer.join() elif event["command"]["type"]=="write": await wirte_file(websocket, key,event["project_name"], event["path"], event["content"], connected) elif event["command"]["type"]=="curl": response = requests.get(event['url']) event = { "type": "web-data", "data": response.text, } await websocket.send(json.dumps(event)) elif event["command"]["type"]=="create": if event["item"]=="folder": await create_folder(websocket, key,event["project_name"], event["path"],event["name"],event['root'],event['targetElementData'],event["rpath"], connected) else: await create_file(websocket, key,event["project_name"], event["path"],event["name"],event['root'],event['targetElementData'],event["rpath"], connected) elif event["command"]["type"]=="delete": await delete_item(websocket, key,event["project_name"], event["path"],event['rpath'],event['targetElementData'], connected) elif event["command"]["type"]=="get_content": await get_file_content(websocket, key,event["project_name"],event["filePath"], event["rfilePath"],event["fileName"] ,connected) elif event["command"]["type"]=="rename": await rename_item(websocket, key,event["project_name"], event["path"],event['rpath'], event["name"], event["root"], connected) elif event["command"]["type"]=="join": await join(websocket, event["join"]) elif event["command"]["type"]=="sendDir": data=json.loads(event["file_structure"]) event = { "type": "createDir", "path": data, "root":event['root'], } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="createItemUI": event = { "type": "createItemUI", 'targetElementData':event['targetElementData'], 'data':event['data'] } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="renameItemInUI": event = { "type": "renameItemInUI", 'path':event['path'], 'new_path':event['new_path'], 'name':event['name'], 'new_rpath':event['new_rpath'], } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="createFolderUI": event = { "type": "createFolderUI", 'targetElementData':event['targetElementData'], 'data':event['data'] } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="removeItemFromUI": event = { "type": "removeItemFromUI", 'targetElementData':event['targetElementData'], 'path':event['path'] } # websockets.broadcast(connected,json.dumps(event)) websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="project": base_path = os.path.join(os.getcwd(), 'projects',key, event["project_name"]) data=json.loads(event["file_structure"]) await create_file_structure(websocket,data, base_path=base_path) # await rename_item(websocket, key,event["project_name"], event["path"], event["name"], connected) elif event["command"]["type"]=="collabration": event = { "type": "collabration", 'name': event["name"], 'line': event["cursorPos-line"], 'ch': event["cursorPos-ch"], 'file':event["file"], 'content': event["content"], 'color':event["color"] } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="cursor": event = { "type": "cursor", 'offset': event["offset"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="select": event = { "type": "select", 'id': event["id"], 'startOffset': event["startOffset"], 'endOffset': event["endOffset"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="addselection": event = { "type": "addselection", 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="insert": event = { "type": "insert", 'index': event["index"], 'text': event["text"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], 'tabStatus':event['tabStatus'], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="replace": event = { "type": "replace", 'index': event["index"], 'length': event["length"], 'text': event["text"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], 'tabStatus':event['tabStatus'], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="edelelte": event = { "type": "edelelte", 'index': event["index"], 'length': event["length"], 'userid': event["userid"], 'userlabel': event["userlabel"], 'usercolor': event["usercolor"], 'fileName':event["fileName"], } websockets.broadcast(connected, json.dumps(event)) elif event["command"]["type"]=="keep-alive": event = { "type": "keep-alive", } else: # First player starts a new game. pass except RuntimeError as exc: # Send an "error" event if the move was illegal. await error(websocket, str(exc)) continue async def start(websocket,events): user = User() connected = {websocket} join_key = secrets.token_urlsafe(12) JOIN[join_key] = user, connected try: # Send the secret access tokens to the browser of the first player, # where they'll be used for building "join" and "watch" links. event = { "type": "init", "join": join_key, } await websocket.send(json.dumps(event)) await exe(websocket,connected,join_key) finally: del JOIN[join_key] async def join(websocket, key): """ Handle a connection from the second player: join an existing game. """ # Find the Connect Four game. try: user, connected = JOIN[key] except KeyError: await error(websocket, "collabration not found.") return # Register to receive moves from this game. connected.add(websocket) try: event = { "type": "sendDir", } websockets.broadcast(connected,json.dumps(event)) await exe(websocket,connected,key) finally: connected.remove(websocket) async def handler(websocket): """ Handle a connection and dispatch it according to who is connecting. """ # Receive and parse the "init" event from the UI. message = await websocket.recv() event = json.loads(message) # print(event) # project_name = event["project_name"] # assert event["type"] == "init" if event["type"] == "init": if "join" in event: # Second player joins an existing game. await join(websocket, event["join"]) else: # First player starts a new game. await start(websocket, message) elif event["type"] == "cmd": # print('executing commad') # Execute a command in the project folder. await execute_command(websocket, event["project_name"], event["command"]) async def main(): # Set the stop condition when receiving SIGTERM. loop = asyncio.get_running_loop() stop = loop.create_future() loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) port = int(os.environ.get("PORT", "7860")) async with websockets.serve(handler, "0.0.0.0", port): await stop if __name__ == "__main__": asyncio.run(main())