From 00c4a7df1541fc1f0fa0eeefe95af5b538e1af80 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Mon, 16 Dec 2024 09:53:42 +0000 Subject: [PATCH 01/10] tmp --- computer/Dockerfile | 96 +++++++++++++++++++++++++++++++++++++++ computer/requirements.txt | 2 + computer/server.py | 40 ++++++++++++++++ computer/xstartup | 15 ++++++ 4 files changed, 153 insertions(+) create mode 100644 computer/Dockerfile create mode 100644 computer/requirements.txt create mode 100644 computer/server.py create mode 100644 computer/xstartup diff --git a/computer/Dockerfile b/computer/Dockerfile new file mode 100644 index 00000000..d2174a2e --- /dev/null +++ b/computer/Dockerfile @@ -0,0 +1,96 @@ +# Use Ubuntu as base image +FROM ubuntu:22.04 + +# Avoid prompts from apt +ENV DEBIAN_FRONTEND=noninteractive + +# Set required environment variables +ENV USER=root +ENV HOME=/root +ENV DISPLAY=:1 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 + +# Install required packages +RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ + xfce4 \ + xfce4-goodies \ + tightvncserver \ + wget \ + net-tools \ + sudo \ + dbus-x11 \ + x11-xserver-utils \ + xfonts-base \ + autocutsel \ + chromium-browser \ + chromium-browser-l10n \ + chromium-codecs-ffmpeg \ + fonts-liberation \ + fonts-noto \ + fonts-noto-cjk \ + fonts-noto-color-emoji \ + locales \ + pulseaudio \ + bzip2 \ + libgtk-3-0 \ + libdbus-glib-1-2 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Download and install Firefox +RUN wget -O firefox.tar.bz2 "https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US" && \ + tar xjf firefox.tar.bz2 -C /opt/ && \ + rm firefox.tar.bz2 && \ + ln -s /opt/firefox/firefox /usr/local/bin/firefox + +# Generate locale +RUN locale-gen en_US.UTF-8 + +# Set up VNC password and directory +RUN mkdir -p /root/.vnc +RUN echo "password" | vncpasswd -f > /root/.vnc/passwd +RUN chmod 600 /root/.vnc/passwd + +# Copy VNC xstartup file +COPY xstartup /root/.vnc/xstartup +RUN chmod +x /root/.vnc/xstartup + +# Install Python requirements +COPY requirements.txt /requirements.txt +RUN pip3 install -r requirements.txt + +# Copy Python server code +COPY server.py /server.py + +# Create desktop shortcuts +RUN mkdir -p /root/Desktop +RUN echo "[Desktop Entry]\n\ + Version=1.0\n\ + Type=Application\n\ + Name=Firefox\n\ + Comment=Browse the Web\n\ + Exec=/usr/local/bin/firefox %u\n\ + Icon=/opt/firefox/browser/chrome/icons/default/default128.png\n\ + Terminal=false\n\ + Categories=Network;WebBrowser;" > /root/Desktop/firefox.desktop + +RUN echo "[Desktop Entry]\n\ + Type=Application\n\ + Name=Chromium\n\ + Comment=Browse the Web\n\ + Exec=chromium-browser %U\n\ + Icon=chromium-browser\n\ + Terminal=false\n\ + Categories=Network;WebBrowser;" > /root/Desktop/chromium.desktop + +RUN chmod +x /root/Desktop/*.desktop + +# Expose VNC port and Python server port +EXPOSE 5901 8080 + +# Start VNC server and Python server +CMD ["bash", "-c", "vncserver :1 -geometry 1280x800 -depth 24 && python3 /server.py"] \ No newline at end of file diff --git a/computer/requirements.txt b/computer/requirements.txt new file mode 100644 index 00000000..a5d6961a --- /dev/null +++ b/computer/requirements.txt @@ -0,0 +1,2 @@ +flask==2.3.3 +werkzeug==2.3.7 \ No newline at end of file diff --git a/computer/server.py b/computer/server.py new file mode 100644 index 00000000..4151f7b0 --- /dev/null +++ b/computer/server.py @@ -0,0 +1,40 @@ +from flask import Flask, request, jsonify +import subprocess +import os + +app = Flask(__name__) + + +@app.route("/execute", methods=["POST"]) +def execute_command(): + data = request.get_json() + command = data.get("command") + + if not command: + return jsonify({"error": "No command provided"}), 400 + + try: + # Execute the command and capture output + process = subprocess.Popen( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + stdout, stderr = process.communicate() + + return jsonify( + {"stdout": stdout, "stderr": stderr, "returncode": process.returncode} + ) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/health", methods=["GET"]) +def health_check(): + return jsonify({"status": "healthy"}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080) diff --git a/computer/xstartup b/computer/xstartup new file mode 100644 index 00000000..e4db0ad9 --- /dev/null +++ b/computer/xstartup @@ -0,0 +1,15 @@ +#!/bin/bash +unset SESSION_MANAGER +unset DBUS_SESSION_BUS_ADDRESS + +[ -x /etc/vnc/xstartup ] && exec /etc/vnc/xstartup +[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources + +# Fix for blank screen issue +export XKL_XMODMAP_DISABLE=1 + +vncconfig -iconic & +autocutsel -fork & + +# Start desktop environment +startxfce4 & \ No newline at end of file From 8ac8b9fc1ef766ed60bc22ac2beb34ef0147fa9b Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Thu, 19 Dec 2024 17:55:46 +0000 Subject: [PATCH 02/10] tmp --- computer/Dockerfile | 96 -------------------------- computer/requirements.txt | 2 - computer/server.py | 40 ----------- computer/xstartup | 15 ---- frontend/app/computer/page.tsx | 5 ++ frontend/components/vnc/vnc-client.tsx | 80 +++++++++++++++++++++ frontend/components/vnc/vnc.tsx | 25 +++++++ frontend/next.config.js | 6 ++ frontend/package.json | 1 + frontend/pnpm-lock.yaml | 8 +++ frontend/types/novnc.d.ts | 7 ++ 11 files changed, 132 insertions(+), 153 deletions(-) delete mode 100644 computer/Dockerfile delete mode 100644 computer/requirements.txt delete mode 100644 computer/server.py delete mode 100644 computer/xstartup create mode 100644 frontend/app/computer/page.tsx create mode 100644 frontend/components/vnc/vnc-client.tsx create mode 100644 frontend/components/vnc/vnc.tsx create mode 100644 frontend/types/novnc.d.ts diff --git a/computer/Dockerfile b/computer/Dockerfile deleted file mode 100644 index d2174a2e..00000000 --- a/computer/Dockerfile +++ /dev/null @@ -1,96 +0,0 @@ -# Use Ubuntu as base image -FROM ubuntu:22.04 - -# Avoid prompts from apt -ENV DEBIAN_FRONTEND=noninteractive - -# Set required environment variables -ENV USER=root -ENV HOME=/root -ENV DISPLAY=:1 -ENV LANG=en_US.UTF-8 -ENV LANGUAGE=en_US:en -ENV LC_ALL=en_US.UTF-8 - -# Install required packages -RUN apt-get update && apt-get install -y \ - python3 \ - python3-pip \ - xfce4 \ - xfce4-goodies \ - tightvncserver \ - wget \ - net-tools \ - sudo \ - dbus-x11 \ - x11-xserver-utils \ - xfonts-base \ - autocutsel \ - chromium-browser \ - chromium-browser-l10n \ - chromium-codecs-ffmpeg \ - fonts-liberation \ - fonts-noto \ - fonts-noto-cjk \ - fonts-noto-color-emoji \ - locales \ - pulseaudio \ - bzip2 \ - libgtk-3-0 \ - libdbus-glib-1-2 \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Download and install Firefox -RUN wget -O firefox.tar.bz2 "https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US" && \ - tar xjf firefox.tar.bz2 -C /opt/ && \ - rm firefox.tar.bz2 && \ - ln -s /opt/firefox/firefox /usr/local/bin/firefox - -# Generate locale -RUN locale-gen en_US.UTF-8 - -# Set up VNC password and directory -RUN mkdir -p /root/.vnc -RUN echo "password" | vncpasswd -f > /root/.vnc/passwd -RUN chmod 600 /root/.vnc/passwd - -# Copy VNC xstartup file -COPY xstartup /root/.vnc/xstartup -RUN chmod +x /root/.vnc/xstartup - -# Install Python requirements -COPY requirements.txt /requirements.txt -RUN pip3 install -r requirements.txt - -# Copy Python server code -COPY server.py /server.py - -# Create desktop shortcuts -RUN mkdir -p /root/Desktop -RUN echo "[Desktop Entry]\n\ - Version=1.0\n\ - Type=Application\n\ - Name=Firefox\n\ - Comment=Browse the Web\n\ - Exec=/usr/local/bin/firefox %u\n\ - Icon=/opt/firefox/browser/chrome/icons/default/default128.png\n\ - Terminal=false\n\ - Categories=Network;WebBrowser;" > /root/Desktop/firefox.desktop - -RUN echo "[Desktop Entry]\n\ - Type=Application\n\ - Name=Chromium\n\ - Comment=Browse the Web\n\ - Exec=chromium-browser %U\n\ - Icon=chromium-browser\n\ - Terminal=false\n\ - Categories=Network;WebBrowser;" > /root/Desktop/chromium.desktop - -RUN chmod +x /root/Desktop/*.desktop - -# Expose VNC port and Python server port -EXPOSE 5901 8080 - -# Start VNC server and Python server -CMD ["bash", "-c", "vncserver :1 -geometry 1280x800 -depth 24 && python3 /server.py"] \ No newline at end of file diff --git a/computer/requirements.txt b/computer/requirements.txt deleted file mode 100644 index a5d6961a..00000000 --- a/computer/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -flask==2.3.3 -werkzeug==2.3.7 \ No newline at end of file diff --git a/computer/server.py b/computer/server.py deleted file mode 100644 index 4151f7b0..00000000 --- a/computer/server.py +++ /dev/null @@ -1,40 +0,0 @@ -from flask import Flask, request, jsonify -import subprocess -import os - -app = Flask(__name__) - - -@app.route("/execute", methods=["POST"]) -def execute_command(): - data = request.get_json() - command = data.get("command") - - if not command: - return jsonify({"error": "No command provided"}), 400 - - try: - # Execute the command and capture output - process = subprocess.Popen( - command, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - stdout, stderr = process.communicate() - - return jsonify( - {"stdout": stdout, "stderr": stderr, "returncode": process.returncode} - ) - except Exception as e: - return jsonify({"error": str(e)}), 500 - - -@app.route("/health", methods=["GET"]) -def health_check(): - return jsonify({"status": "healthy"}) - - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=8080) diff --git a/computer/xstartup b/computer/xstartup deleted file mode 100644 index e4db0ad9..00000000 --- a/computer/xstartup +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -unset SESSION_MANAGER -unset DBUS_SESSION_BUS_ADDRESS - -[ -x /etc/vnc/xstartup ] && exec /etc/vnc/xstartup -[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources - -# Fix for blank screen issue -export XKL_XMODMAP_DISABLE=1 - -vncconfig -iconic & -autocutsel -fork & - -# Start desktop environment -startxfce4 & \ No newline at end of file diff --git a/frontend/app/computer/page.tsx b/frontend/app/computer/page.tsx new file mode 100644 index 00000000..9a2af918 --- /dev/null +++ b/frontend/app/computer/page.tsx @@ -0,0 +1,5 @@ +import VNC from '@/components/vnc/vnc'; + +export default function ComputerPage() { + return ; +} diff --git a/frontend/components/vnc/vnc-client.tsx b/frontend/components/vnc/vnc-client.tsx new file mode 100644 index 00000000..ff94f0b7 --- /dev/null +++ b/frontend/components/vnc/vnc-client.tsx @@ -0,0 +1,80 @@ +'use client'; + +import React, { useEffect, useRef, useState } from 'react'; + +export default function VNCClient({ url, credentials = {}, onConnect, onDisconnect, onError }: + { url: string, credentials?: {}, onConnect: () => void, onDisconnect: () => void, onError: (error: any) => void }) { + const canvasRef = useRef(null); + const rfbRef = useRef(null); + + useEffect(() => { + console.log('VNCClient useEffect'); + if (!canvasRef.current) return; + + let isConnected = false; + + const initVNC = async () => { + try { + const NoVNC = (await import('@novnc/novnc/lib/rfb')).default; + + if (!canvasRef.current) return; + + rfbRef.current = new NoVNC(canvasRef.current, url, { + credentials, + }); + + const handleConnect = () => { + console.log('Connected to VNC server'); + isConnected = true; + onConnect?.(); + }; + + const handleDisconnect = () => { + console.log('Disconnected from VNC server'); + isConnected = false; + onDisconnect?.(); + }; + + const handleError = (error: Error) => { + console.error('VNC Error:', error); + onError?.(error); + }; + + rfbRef.current.addEventListener('connect', handleConnect); + rfbRef.current.addEventListener('disconnect', handleDisconnect); + rfbRef.current.addEventListener('error', handleError); + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to initialize VNC'); + console.error('VNC initialization error:', error); + onError?.(error); + } + }; + + initVNC(); + + return () => { + if (rfbRef.current) { + try { + if (isConnected) { + rfbRef.current.disconnect(); + } + rfbRef.current = null; + } catch (err) { + console.error('Error during cleanup:', err); + } + } + }; + }, [url, credentials, onConnect, onDisconnect, onError, canvasRef]); + + return ( +
+
+
+ ); +} diff --git a/frontend/components/vnc/vnc.tsx b/frontend/components/vnc/vnc.tsx new file mode 100644 index 00000000..d90f676e --- /dev/null +++ b/frontend/components/vnc/vnc.tsx @@ -0,0 +1,25 @@ +'use client'; + +import VNCClient from "./vnc-client"; + +export default function VNC() { + return ( +
+ { + console.log('Connected to VNC server'); + }} + onDisconnect={() => { + console.log('Disconnected from VNC server'); + }} + onError={(error) => { + console.error('VNC Error:', error); + }} + /> +
); +} \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.js index 2fdb6367..471d1d2d 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -34,6 +34,12 @@ const nextConfig = { // Ensure that all imports of 'yjs' resolve to the same instance config.resolve.alias['yjs'] = path.resolve(__dirname, 'node_modules/yjs'); } + + if (isServer) { + config.externals.push({ + 'canvas': 'commonjs canvas' + }) + } return config; }, }; diff --git a/frontend/package.json b/frontend/package.json index 6a09d312..4e046e28 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "@lezer/highlight": "^1.2.1", "@monaco-editor/react": "^4.6.0", "@mux/mux-player-react": "^2.9.1", + "@novnc/novnc": "^1.5.0", "@pnpm/types": "^9.4.2", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-alert-dialog": "^1.1.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 8681e3eb..c49810e9 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@mux/mux-player-react': specifier: ^2.9.1 version: 2.9.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@novnc/novnc': + specifier: ^1.5.0 + version: 1.5.0 '@pnpm/types': specifier: ^9.4.2 version: 9.4.2 @@ -1538,6 +1541,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@novnc/novnc@1.5.0': + resolution: {integrity: sha512-4yGHOtUCnEJUCsgEt/L78eeJu00kthurLBWXFiaXfonNx0pzbs6R/3gJb1byZe6iAE8V9MF0syQb0xIL8MSOtQ==} + '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} @@ -7479,6 +7485,8 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@novnc/novnc@1.5.0': {} + '@one-ini/wasm@0.1.1': {} '@panva/hkdf@1.2.1': {} diff --git a/frontend/types/novnc.d.ts b/frontend/types/novnc.d.ts new file mode 100644 index 00000000..9d76a6eb --- /dev/null +++ b/frontend/types/novnc.d.ts @@ -0,0 +1,7 @@ +declare module '@novnc/novnc/lib/rfb' { + export default class RFB { + constructor(target: HTMLElement, url: string, options?: any); + addEventListener(event: string, callback: Function): void; + disconnect(): void; + } +} \ No newline at end of file From 5d902122af1827fe19b1a94165a34ccc3b22cce6 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Thu, 26 Dec 2024 18:36:05 +0000 Subject: [PATCH 03/10] tmp --- app-server/Cargo.lock | 54 +++- app-server/Cargo.toml | 3 + app-server/build.rs | 9 + app-server/proto/machine_manager_grpc.proto | 54 ++++ app-server/src/api/v1/machine_manager.rs | 207 +++++++++++++ app-server/src/api/v1/mod.rs | 1 + app-server/src/db/machine_manager.rs | 25 ++ app-server/src/db/mod.rs | 1 + .../machine_manager_service_grpc.rs | 277 ++++++++++++++++++ app-server/src/machine_manager/mod.rs | 85 ++++++ app-server/src/main.rs | 23 ++ frontend/components/vnc/vnc-client.tsx | 3 +- frontend/components/vnc/vnc.tsx | 30 +- 13 files changed, 762 insertions(+), 10 deletions(-) create mode 100644 app-server/proto/machine_manager_grpc.proto create mode 100644 app-server/src/api/v1/machine_manager.rs create mode 100644 app-server/src/db/machine_manager.rs create mode 100644 app-server/src/machine_manager/machine_manager_service_grpc.rs create mode 100644 app-server/src/machine_manager/mod.rs diff --git a/app-server/Cargo.lock b/app-server/Cargo.lock index 7e90a7a5..26fa6111 100644 --- a/app-server/Cargo.lock +++ b/app-server/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "actix-codec" @@ -239,6 +239,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-ws" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99" +dependencies = [ + "actix-codec", + "actix-http", + "actix-web", + "bytestring", + "futures-core", + "tokio", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -435,6 +449,7 @@ dependencies = [ "actix-service", "actix-web", "actix-web-httpauth", + "actix-ws", "anyhow", "async-stream", "async-trait", @@ -485,6 +500,7 @@ dependencies = [ "time", "tokio", "tokio-stream", + "tokio-tungstenite", "tonic", "tonic-build", "url", @@ -5799,6 +5815,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.12" @@ -6010,6 +6038,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -6102,6 +6148,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_iter" version = "1.0.5" diff --git a/app-server/Cargo.toml b/app-server/Cargo.toml index e41ddd8f..d1eb9366 100644 --- a/app-server/Cargo.toml +++ b/app-server/Cargo.toml @@ -66,6 +66,9 @@ sha3 = "0.10.8" aws-sdk-s3 = "1.63.0" base64 = "0.22.1" sodiumoxide = "0.2.7" +actix-ws = "0.3.0" +tokio-tungstenite = "*" + [build-dependencies] tonic-build = "0.12.3" diff --git a/app-server/build.rs b/app-server/build.rs index c5d781b5..5deae4f4 100644 --- a/app-server/build.rs +++ b/app-server/build.rs @@ -17,6 +17,15 @@ fn main() -> Result<(), Box> { .out_dir("./src/code_executor/") .compile_protos(&[proto_file], &["proto"])?; + let proto_file = "./proto/machine_manager_grpc.proto"; + + tonic_build::configure() + .protoc_arg("--experimental_allow_proto3_optional") // for older systems + .build_client(true) + .build_server(false) + .out_dir("./src/machine_manager/") + .compile_protos(&[proto_file], &["proto"])?; + tonic_build::configure() .protoc_arg("--experimental_allow_proto3_optional") // for older systems .build_client(false) diff --git a/app-server/proto/machine_manager_grpc.proto b/app-server/proto/machine_manager_grpc.proto new file mode 100644 index 00000000..b7c8ebc6 --- /dev/null +++ b/app-server/proto/machine_manager_grpc.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; +package machine_manager_service_grpc; + +message StartMachineRequest {} + +message StartMachineResponse { + string machine_id = 1; +} + +enum ComputerAction { + KEY = 0; + TYPE = 1; + MOUSE_MOVE = 2; + LEFT_CLICK = 3; + LEFT_CLICK_DRAG = 4; + RIGHT_CLICK = 5; + MIDDLE_CLICK = 6; + DOUBLE_CLICK = 7; + SCREENSHOT = 8; + CURSOR_POSITION = 9; +} + +message ComputerActionCoordinate { + int32 x = 1; + int32 y = 2; +} + +message ComputerActionRequest { + string machine_id = 1; + ComputerAction action = 2; + optional string text = 3; + optional ComputerActionCoordinate coordinates = 4; +} + +message ComputerActionResponse { + optional string output = 1; + optional string error = 2; + optional string base64_image = 3; + optional string system = 4; +} + +message TerminateMachineRequest { + string machine_id = 1; +} + +message TerminateMachineResponse { + bool success = 1; +} + +service MachineManagerService { + rpc StartMachine(StartMachineRequest) returns (StartMachineResponse); + rpc TerminateMachine(TerminateMachineRequest) returns (TerminateMachineResponse); + rpc ExecuteComputerAction(ComputerActionRequest) returns (ComputerActionResponse); +} \ No newline at end of file diff --git a/app-server/src/api/v1/machine_manager.rs b/app-server/src/api/v1/machine_manager.rs new file mode 100644 index 00000000..2cfe639a --- /dev/null +++ b/app-server/src/api/v1/machine_manager.rs @@ -0,0 +1,207 @@ +use crate::db::project_api_keys::ProjectApiKey; +use crate::db::{self, DB}; +use crate::machine_manager::{ + ComputerActionCoordinate as MachineManagerComputerActionCoordinate, + ComputerActionRequest as MachineManagerComputerActionRequest, +}; +use crate::{machine_manager::MachineManager, routes::types::ResponseResult}; +use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder}; +use futures_util::{SinkExt, StreamExt}; +use log::{error, info}; +use serde::Deserialize; +use serde::Serialize; +use serde_json::json; +use std::env; +use std::sync::Arc; +use uuid::Uuid; + +#[post("machine/start")] +pub async fn start_machine( + machine_manager: web::Data>, + project_api_key: ProjectApiKey, + db: web::Data, +) -> ResponseResult { + let project_id = project_api_key.project_id; + let machine_id = machine_manager.start_machine().await?; + + db::machine_manager::create_machine(&db.pool, machine_id, project_id).await?; + + Ok(HttpResponse::Ok().json(json!({ "machine_id": machine_id }))) +} + +#[derive(Deserialize)] +struct TerminateMachineRequest { + machine_id: Uuid, +} + +#[post("machine/terminate")] +pub async fn terminate_machine( + machine_manager: web::Data>, + request: web::Json, + project_api_key: ProjectApiKey, + db: web::Data, +) -> ResponseResult { + let request = request.into_inner(); + let project_id = project_api_key.project_id; + machine_manager + .terminate_machine(request.machine_id) + .await?; + + db::machine_manager::delete_machine(&db.pool, request.machine_id, project_id).await?; + Ok(HttpResponse::Ok().json(json!({ "success": true }))) +} + +#[derive(Deserialize)] +enum ComputerAction { + Key = 0, + Type = 1, + MouseMove = 2, + LeftClick = 3, + LeftClickDrag = 4, + RightClick = 5, + MiddleClick = 6, + DoubleClick = 7, + Screenshot = 8, + CursorPosition = 9, +} + +#[derive(Deserialize)] +struct Coordinates { + x: u32, + y: u32, +} + +#[derive(Deserialize)] +struct ComputerActionRequest { + action: ComputerAction, + coordinates: Option, + text: Option, + machine_id: String, +} + +#[derive(Serialize)] +struct ComputerActionResponse { + output: Option, + error: Option, + base64_image: Option, + system: Option, +} + +#[post("machine/computer_action")] +pub async fn execute_computer_action( + machine_manager: web::Data>, + request: web::Json, +) -> ResponseResult { + let request = request.into_inner(); + + let coordinates = if let Some(coordinates) = request.coordinates { + Some(MachineManagerComputerActionCoordinate { + x: coordinates.x as i32, + y: coordinates.y as i32, + }) + } else { + None + }; + + let computer_action_request = MachineManagerComputerActionRequest { + action: request.action as i32, + coordinates, + text: request.text, + machine_id: request.machine_id, + }; + + let response = machine_manager + .execute_computer_action(computer_action_request) + .await?; + + let response = ComputerActionResponse { + output: response.output, + error: response.error, + base64_image: response.base64_image, + system: response.system, + }; + + Ok(HttpResponse::Ok().json(response)) +} + +#[get("v1/machine/vnc_stream/{machine_id}")] +pub async fn vnc_stream( + machine_id: web::Path, + body: web::Payload, + req: HttpRequest, +) -> actix_web::Result { + let machine_id = machine_id.into_inner(); + // Set up WebSocket connection + let (response, mut client_session, mut client_msg_stream) = actix_ws::handle(&req, body)?; + + let machine_manager_url_ws = + env::var("MACHINE_MANAGER_URL_WS").expect("MACHINE_MANAGER_URL_WS is not set"); + // Connect to VNC machine + let machine_url = format!("{}/{}", machine_manager_url_ws, machine_id); + + let (machine_ws_stream, _) = match tokio_tungstenite::connect_async(&machine_url).await { + Ok(conn) => conn, + Err(e) => { + error!("Failed to connect to VNC machine at {}: {}", machine_url, e); + return Ok(actix_web::HttpResponse::ServiceUnavailable().finish()); + } + }; + + let (mut machine_write, mut machine_read) = machine_ws_stream.split(); + + // Forward machine messages to client + actix_web::rt::spawn(async move { + info!("Starting machine to client forwarding task"); + while let Some(Ok(msg)) = machine_read.next().await { + match msg { + tokio_tungstenite::tungstenite::Message::Text(text) => { + if let Err(e) = client_session.text(text.to_string()).await { + error!("Error forwarding text message to client: {}", e); + break; + } + } + tokio_tungstenite::tungstenite::Message::Binary(bytes) => { + if let Err(e) = client_session.binary(bytes).await { + error!("Error forwarding binary message to client: {}", e); + break; + } + } + tokio_tungstenite::tungstenite::Message::Ping(bytes) => { + if let Err(e) = client_session.pong(&bytes).await { + error!("Error sending pong to client: {}", e); + break; + } + } + tokio_tungstenite::tungstenite::Message::Close(_) => break, + _ => {} + } + } + }); + + // Forward client messages to machine + actix_web::rt::spawn(async move { + info!("Starting client to machine forwarding task"); + while let Some(Ok(msg)) = client_msg_stream.next().await { + let machine_msg = match msg { + actix_ws::Message::Text(text) => { + tokio_tungstenite::tungstenite::Message::Text(text.to_string().into()) + } + actix_ws::Message::Binary(bytes) => { + tokio_tungstenite::tungstenite::Message::Binary(bytes.to_vec()) + } + actix_ws::Message::Ping(bytes) => { + tokio_tungstenite::tungstenite::Message::Ping(bytes.to_vec()) + } + actix_ws::Message::Close(_) => tokio_tungstenite::tungstenite::Message::Close(None), + _ => continue, + }; + + if let Err(e) = machine_write.send(machine_msg).await { + error!("Error forwarding message to machine: {}", e); + break; + } + } + }); + + Ok(response) +} diff --git a/app-server/src/api/v1/mod.rs b/app-server/src/api/v1/mod.rs index 72e14251..e8241fbf 100644 --- a/app-server/src/api/v1/mod.rs +++ b/app-server/src/api/v1/mod.rs @@ -1,5 +1,6 @@ pub mod datasets; pub mod evaluations; +pub mod machine_manager; pub mod metrics; pub mod pipelines; pub mod semantic_search; diff --git a/app-server/src/db/machine_manager.rs b/app-server/src/db/machine_manager.rs new file mode 100644 index 00000000..ab956b5b --- /dev/null +++ b/app-server/src/db/machine_manager.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use sqlx::PgPool; +use uuid::Uuid; + +pub async fn create_machine(pool: &PgPool, machine_id: Uuid, project_id: Uuid) -> Result<()> { + sqlx::query!( + r"INSERT INTO machines (id, project_id) VALUES ($1, $2)", + machine_id, + project_id + ) + .execute(pool) + .await?; + Ok(()) +} + +pub async fn delete_machine(pool: &PgPool, machine_id: Uuid, project_id: Uuid) -> Result<()> { + sqlx::query!( + r"DELETE FROM machines WHERE id = $1 AND project_id = $2", + machine_id, + project_id + ) + .execute(pool) + .await?; + Ok(()) +} diff --git a/app-server/src/db/mod.rs b/app-server/src/db/mod.rs index 111a5f96..cd760cb3 100644 --- a/app-server/src/db/mod.rs +++ b/app-server/src/db/mod.rs @@ -6,6 +6,7 @@ pub mod evaluations; pub mod events; pub mod labeling_queues; pub mod labels; +pub mod machine_manager; pub mod modifiers; pub mod pipelines; pub mod prices; diff --git a/app-server/src/machine_manager/machine_manager_service_grpc.rs b/app-server/src/machine_manager/machine_manager_service_grpc.rs new file mode 100644 index 00000000..6b0d1a71 --- /dev/null +++ b/app-server/src/machine_manager/machine_manager_service_grpc.rs @@ -0,0 +1,277 @@ +// This file is @generated by prost-build. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct StartMachineRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct StartMachineResponse { + #[prost(string, tag = "1")] + pub machine_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct ComputerActionCoordinate { + #[prost(int32, tag = "1")] + pub x: i32, + #[prost(int32, tag = "2")] + pub y: i32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ComputerActionRequest { + #[prost(string, tag = "1")] + pub machine_id: ::prost::alloc::string::String, + #[prost(enumeration = "ComputerAction", tag = "2")] + pub action: i32, + #[prost(string, optional, tag = "3")] + pub text: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, optional, tag = "4")] + pub coordinates: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ComputerActionResponse { + #[prost(string, optional, tag = "1")] + pub output: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "2")] + pub error: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "3")] + pub base64_image: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "4")] + pub system: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TerminateMachineRequest { + #[prost(string, tag = "1")] + pub machine_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct TerminateMachineResponse { + #[prost(bool, tag = "1")] + pub success: bool, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ComputerAction { + Key = 0, + Type = 1, + MouseMove = 2, + LeftClick = 3, + LeftClickDrag = 4, + RightClick = 5, + MiddleClick = 6, + DoubleClick = 7, + Screenshot = 8, + CursorPosition = 9, +} +impl ComputerAction { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Key => "KEY", + Self::Type => "TYPE", + Self::MouseMove => "MOUSE_MOVE", + Self::LeftClick => "LEFT_CLICK", + Self::LeftClickDrag => "LEFT_CLICK_DRAG", + Self::RightClick => "RIGHT_CLICK", + Self::MiddleClick => "MIDDLE_CLICK", + Self::DoubleClick => "DOUBLE_CLICK", + Self::Screenshot => "SCREENSHOT", + Self::CursorPosition => "CURSOR_POSITION", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "KEY" => Some(Self::Key), + "TYPE" => Some(Self::Type), + "MOUSE_MOVE" => Some(Self::MouseMove), + "LEFT_CLICK" => Some(Self::LeftClick), + "LEFT_CLICK_DRAG" => Some(Self::LeftClickDrag), + "RIGHT_CLICK" => Some(Self::RightClick), + "MIDDLE_CLICK" => Some(Self::MiddleClick), + "DOUBLE_CLICK" => Some(Self::DoubleClick), + "SCREENSHOT" => Some(Self::Screenshot), + "CURSOR_POSITION" => Some(Self::CursorPosition), + _ => None, + } + } +} +/// Generated client implementations. +pub mod machine_manager_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct MachineManagerServiceClient { + inner: tonic::client::Grpc, + } + impl MachineManagerServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl MachineManagerServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> MachineManagerServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + MachineManagerServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn start_machine( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/machine_manager_service_grpc.MachineManagerService/StartMachine", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "machine_manager_service_grpc.MachineManagerService", + "StartMachine", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn terminate_machine( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/machine_manager_service_grpc.MachineManagerService/TerminateMachine", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "machine_manager_service_grpc.MachineManagerService", + "TerminateMachine", + ), + ); + self.inner.unary(req, path, codec).await + } + pub async fn execute_computer_action( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/machine_manager_service_grpc.MachineManagerService/ExecuteComputerAction", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "machine_manager_service_grpc.MachineManagerService", + "ExecuteComputerAction", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} diff --git a/app-server/src/machine_manager/mod.rs b/app-server/src/machine_manager/mod.rs new file mode 100644 index 00000000..51b7ee81 --- /dev/null +++ b/app-server/src/machine_manager/mod.rs @@ -0,0 +1,85 @@ +mod machine_manager_service_grpc; + +use anyhow::Result; +use async_trait::async_trait; +use machine_manager_service_client::MachineManagerServiceClient; +pub use machine_manager_service_grpc::*; +use sqlx::pool; +use std::sync::Arc; +use tonic::transport::Channel; +use uuid::Uuid; + +#[async_trait] +pub trait MachineManager: Send + Sync { + async fn start_machine(&self) -> Result; + + async fn terminate_machine(&self, machine_id: Uuid) -> Result<()>; + + async fn execute_computer_action( + &self, + request: ComputerActionRequest, + ) -> Result; +} + +pub struct MachineManagerImpl { + client: Arc>, +} + +impl MachineManagerImpl { + pub fn new(client: Arc>) -> Self { + Self { client } + } +} + +#[async_trait] +impl MachineManager for MachineManagerImpl { + async fn start_machine(&self) -> Result { + let mut client = self.client.as_ref().clone(); + let request = tonic::Request::new(StartMachineRequest {}); + let response = client.start_machine(request).await?; + + let machine_id = Uuid::parse_str(&response.into_inner().machine_id)?; + Ok(machine_id) + } + + async fn terminate_machine(&self, machine_id: Uuid) -> Result<()> { + let mut client = self.client.as_ref().clone(); + let request = tonic::Request::new(TerminateMachineRequest { + machine_id: machine_id.to_string(), + }); + + client.terminate_machine(request).await?; + + Ok(()) + } + + async fn execute_computer_action( + &self, + request: ComputerActionRequest, + ) -> Result { + let mut client = self.client.as_ref().clone(); + let request = tonic::Request::new(request); + let response = client.execute_computer_action(request).await?; + Ok(response.into_inner()) + } +} + +pub struct MockMachineManager {} + +#[async_trait] +impl MachineManager for MockMachineManager { + async fn start_machine(&self) -> Result { + todo!() + } + + async fn terminate_machine(&self, _machine_id: Uuid) -> Result<()> { + todo!() + } + + async fn execute_computer_action( + &self, + _request: ComputerActionRequest, + ) -> Result { + todo!() + } +} diff --git a/app-server/src/main.rs b/app-server/src/main.rs index cbdf5561..61aef429 100644 --- a/app-server/src/main.rs +++ b/app-server/src/main.rs @@ -10,6 +10,9 @@ use code_executor::{code_executor_grpc::code_executor_client::CodeExecutorClient use dashmap::DashMap; use db::{pipelines::PipelineVersion, project_api_keys::ProjectApiKey, user::User}; use features::{is_feature_enabled, Feature}; +use machine_manager::{ + machine_manager_service_client::MachineManagerServiceClient, MachineManager, MachineManagerImpl, +}; use names::NameGenerator; use opentelemetry::opentelemetry::proto::collector::trace::v1::trace_service_server::TraceServiceServer; use projects::Project; @@ -62,6 +65,7 @@ mod evaluations; mod features; mod labels; mod language_model; +mod machine_manager; mod names; mod opentelemetry; mod pipeline; @@ -286,6 +290,20 @@ fn main() -> anyhow::Result<()> { Arc::new(code_executor::mock::MockCodeExecutor {}) }; + let machine_manager: Arc = + if is_feature_enabled(Feature::FullBuild) { + let machine_manager_url_grpc = env::var("MACHINE_MANAGER_URL_GRPC") + .expect("MACHINE_MANAGER_URL_GRPC must be set"); + let machine_manager_client = Arc::new( + MachineManagerServiceClient::connect(machine_manager_url_grpc) + .await + .unwrap(), + ); + Arc::new(MachineManagerImpl::new(machine_manager_client)) + } else { + Arc::new(machine_manager::MockMachineManager {}) + }; + let client = reqwest::Client::new(); let anthropic = language_model::Anthropic::new(client.clone()); let openai = language_model::OpenAI::new(client.clone()); @@ -378,6 +396,7 @@ fn main() -> anyhow::Result<()> { .app_data(web::Data::new(semantic_search.clone())) .app_data(web::Data::new(chunker_runner.clone())) .app_data(web::Data::new(storage.clone())) + .app_data(web::Data::new(machine_manager.clone())) // Scopes with specific auth or no auth .service( web::scope("api/v1/auth") @@ -394,6 +413,7 @@ fn main() -> anyhow::Result<()> { .wrap(shared_secret_auth) .service(routes::subscriptions::update_subscription), ) + .service(api::v1::machine_manager::vnc_stream) // vnc stream does not need auth .service( web::scope("/v1") .wrap(project_auth.clone()) @@ -404,6 +424,9 @@ fn main() -> anyhow::Result<()> { .service(api::v1::evaluations::create_evaluation) .service(api::v1::metrics::process_metrics) .service(api::v1::semantic_search::semantic_search) + .service(api::v1::machine_manager::start_machine) + .service(api::v1::machine_manager::terminate_machine) + .service(api::v1::machine_manager::execute_computer_action) .app_data(PayloadConfig::new(10 * 1024 * 1024)), ) // Scopes with generic auth diff --git a/frontend/components/vnc/vnc-client.tsx b/frontend/components/vnc/vnc-client.tsx index ff94f0b7..3d20b42f 100644 --- a/frontend/components/vnc/vnc-client.tsx +++ b/frontend/components/vnc/vnc-client.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef } from 'react'; export default function VNCClient({ url, credentials = {}, onConnect, onDisconnect, onError }: { url: string, credentials?: {}, onConnect: () => void, onDisconnect: () => void, onError: (error: any) => void }) { @@ -8,7 +8,6 @@ export default function VNCClient({ url, credentials = {}, onConnect, onDisconne const rfbRef = useRef(null); useEffect(() => { - console.log('VNCClient useEffect'); if (!canvasRef.current) return; let isConnected = false; diff --git a/frontend/components/vnc/vnc.tsx b/frontend/components/vnc/vnc.tsx index d90f676e..2ea22647 100644 --- a/frontend/components/vnc/vnc.tsx +++ b/frontend/components/vnc/vnc.tsx @@ -1,16 +1,31 @@ 'use client'; +import { useState } from "react"; + import VNCClient from "./vnc-client"; export default function VNC() { + const [id, setId] = useState(""); + return (
+
+ setId(e.target.value)} + className="border p-2 rounded mr-2" + placeholder="Enter VNC ID" + /> + +
{ console.log('Connected to VNC server'); }} @@ -21,5 +36,6 @@ export default function VNC() { console.error('VNC Error:', error); }} /> -
); -} \ No newline at end of file +
+ ); +} From bbadbee4915d20209c35a1b75d72bd3bc79f6cd7 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Thu, 26 Dec 2024 21:14:21 +0000 Subject: [PATCH 04/10] tmp --- frontend/components/vnc/vnc.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/vnc/vnc.tsx b/frontend/components/vnc/vnc.tsx index 2ea22647..f846c8ea 100644 --- a/frontend/components/vnc/vnc.tsx +++ b/frontend/components/vnc/vnc.tsx @@ -25,7 +25,7 @@ export default function VNC() { { console.log('Connected to VNC server'); }} From 1416afec16322826b77f72f8fb6416a38209d812 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Thu, 26 Dec 2024 21:52:42 +0000 Subject: [PATCH 05/10] actions --- .github/workflows/backend-build.yml | 39 ++++++++++++++++++++ app-server/Dockerfile | 55 +++++++++++++++++++---------- 2 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/backend-build.yml diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml new file mode 100644 index 00000000..1753275d --- /dev/null +++ b/.github/workflows/backend-build.yml @@ -0,0 +1,39 @@ +name: Backend build + +on: + pull_request: + types: + - synchronize + - opened + - reopened + paths: + - 'app-server/**' + +jobs: + build-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-app-server-pr-${{ hashFiles('Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-buildx-app-server-pr- + + - name: Build and push image to AWS ECR + uses: docker/build-push-action@v6 + with: + context: . + file: ./app-server/Dockerfile + push: false + platforms: linux/amd64 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache,mode=max \ No newline at end of file diff --git a/app-server/Dockerfile b/app-server/Dockerfile index 9bf6aa14..4f5b4a7c 100644 --- a/app-server/Dockerfile +++ b/app-server/Dockerfile @@ -1,30 +1,47 @@ -# Uses the bullseye-slim debian image per the rust recommendation. -FROM rust:1.81-slim-bullseye AS builder - -# Install g++ and other build essentials for compiling openssl/tls dependencies -RUN apt update -RUN apt install -y build-essential +# Build stage with cargo chef for dependency caching +FROM rust:1.81-slim-bullseye AS chef +WORKDIR /app-server -# Install other openssl / native tls dependencies -RUN apt-get update -RUN apt-get install -y \ +# Install build dependencies and cargo-chef +RUN apt-get update && apt-get install -y \ + build-essential \ pkg-config \ libssl-dev \ protobuf-compiler \ libfontconfig1-dev \ - libfontconfig \ - libclang-dev + libfontconfig \ + libclang-dev \ + && rm -rf /var/lib/apt/lists/* \ + && cargo install cargo-chef -# Clean up some unnecessary apt artifacts -RUN rm -rf /var/lib/apt/lists/* - -WORKDIR /app-server +# Prepare recipe for dependency caching +FROM chef AS planner COPY . . +RUN cargo chef prepare --recipe-path recipe.json -EXPOSE 8000 -ARG DATABASE_URL -ENV DATABASE_URL=${DATABASE_URL} +# Build dependencies - this layer is cached +FROM chef AS builder +COPY --from=planner /app-server/recipe.json recipe.json +# Build dependencies +RUN cargo chef cook --release --recipe-path recipe.json + +# Build application +COPY . . ENV SQLX_OFFLINE=true RUN cargo build --release --all -CMD ["./target/release/app-server"] \ No newline at end of file +# Final runtime stage +FROM debian:bullseye-slim AS runtime +WORKDIR /app-server + +# Install only runtime dependencies +RUN apt-get update && apt-get install -y \ + libssl1.1 \ + libfontconfig1 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app-server/target/release/app-server . + +EXPOSE 8000 + +CMD ["./app-server"] \ No newline at end of file From 2b7f30cd96e795ad9d5a7cb6ecc454e9ae03b1a3 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Thu, 26 Dec 2024 22:04:17 +0000 Subject: [PATCH 06/10] fix --- .github/workflows/backend-build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 1753275d..21ea2e60 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -31,8 +31,7 @@ jobs: - name: Build and push image to AWS ECR uses: docker/build-push-action@v6 with: - context: . - file: ./app-server/Dockerfile + context: ./app-server push: false platforms: linux/amd64 cache-from: type=local,src=/tmp/.buildx-cache From 7bc33c630c0d57a31f9ff74a6fcae8b66b3d2522 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Fri, 27 Dec 2024 11:20:10 +0000 Subject: [PATCH 07/10] removed sqlx offline --- app-server/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/app-server/Dockerfile b/app-server/Dockerfile index 4f5b4a7c..74f4d8c9 100644 --- a/app-server/Dockerfile +++ b/app-server/Dockerfile @@ -27,7 +27,6 @@ RUN cargo chef cook --release --recipe-path recipe.json # Build application COPY . . -ENV SQLX_OFFLINE=true RUN cargo build --release --all # Final runtime stage From d9a36aa29390b420b03d2acbe9b02ce1db8c313c Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Fri, 27 Dec 2024 11:31:27 +0000 Subject: [PATCH 08/10] removed macros --- app-server/src/db/machine_manager.rs | 24 ++++++++++-------------- app-server/src/machine_manager/mod.rs | 1 - 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app-server/src/db/machine_manager.rs b/app-server/src/db/machine_manager.rs index ab956b5b..d220c1f9 100644 --- a/app-server/src/db/machine_manager.rs +++ b/app-server/src/db/machine_manager.rs @@ -3,23 +3,19 @@ use sqlx::PgPool; use uuid::Uuid; pub async fn create_machine(pool: &PgPool, machine_id: Uuid, project_id: Uuid) -> Result<()> { - sqlx::query!( - r"INSERT INTO machines (id, project_id) VALUES ($1, $2)", - machine_id, - project_id - ) - .execute(pool) - .await?; + sqlx::query("INSERT INTO machines (id, project_id) VALUES ($1, $2)") + .bind(machine_id) + .bind(project_id) + .execute(pool) + .await?; Ok(()) } pub async fn delete_machine(pool: &PgPool, machine_id: Uuid, project_id: Uuid) -> Result<()> { - sqlx::query!( - r"DELETE FROM machines WHERE id = $1 AND project_id = $2", - machine_id, - project_id - ) - .execute(pool) - .await?; + sqlx::query("DELETE FROM machines WHERE id = $1 AND project_id = $2") + .bind(machine_id) + .bind(project_id) + .execute(pool) + .await?; Ok(()) } diff --git a/app-server/src/machine_manager/mod.rs b/app-server/src/machine_manager/mod.rs index 51b7ee81..62de3547 100644 --- a/app-server/src/machine_manager/mod.rs +++ b/app-server/src/machine_manager/mod.rs @@ -4,7 +4,6 @@ use anyhow::Result; use async_trait::async_trait; use machine_manager_service_client::MachineManagerServiceClient; pub use machine_manager_service_grpc::*; -use sqlx::pool; use std::sync::Arc; use tonic::transport::Channel; use uuid::Uuid; From 4d01fcb6d26024626ba40825031f072f6b5ba598 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Mon, 30 Dec 2024 21:38:41 +0500 Subject: [PATCH 09/10] dockerfile --- app-server/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app-server/Dockerfile b/app-server/Dockerfile index 74f4d8c9..d8681ac9 100644 --- a/app-server/Dockerfile +++ b/app-server/Dockerfile @@ -37,10 +37,14 @@ WORKDIR /app-server RUN apt-get update && apt-get install -y \ libssl1.1 \ libfontconfig1 \ + ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /app-server/target/release/app-server . +# Copy data files for name generation +COPY data/ /app-server/data/ EXPOSE 8000 +EXPOSE 8001 -CMD ["./app-server"] \ No newline at end of file +CMD ["./app-server"] From 2511e434bd296078794c259a07ab6604713248b7 Mon Sep 17 00:00:00 2001 From: Robert Kim Date: Mon, 6 Jan 2025 02:26:44 +0500 Subject: [PATCH 10/10] fix --- .github/workflows/backend-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 21ea2e60..dfd8f462 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -28,7 +28,7 @@ jobs: restore-keys: | ${{ runner.os }}-buildx-app-server-pr- - - name: Build and push image to AWS ECR + - name: Build Docker image uses: docker/build-push-action@v6 with: context: ./app-server