diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b3e0b49 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,149 @@ +name: Build Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + # 构建 Linux 服务端 (musl 静态链接,兼容所有 Linux 发行版) + build-server-linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl + + - name: Install musl-tools + run: sudo apt-get update && sudo apt-get install -y musl-tools + + - name: Build Server (musl static) + run: cargo build --release --package easyremote-server --target x86_64-unknown-linux-musl + + - name: Prepare release + run: | + mkdir -p release/server + cp target/x86_64-unknown-linux-musl/release/easyremote-server release/server/ + cp -r crates/server/static release/server/ + echo '#!/bin/bash' > release/server/start.sh + echo 'cd "$(dirname "$0")"' >> release/server/start.sh + echo 'chmod +x easyremote-server' >> release/server/start.sh + echo './easyremote-server' >> release/server/start.sh + chmod +x release/server/start.sh + cd release && tar -czvf ../easyremote-server-linux-x86_64.tar.gz server/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: easyremote-server-linux-x86_64 + path: easyremote-server-linux-x86_64.tar.gz + + # 构建 Windows 服务端 + build-server-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + + - name: Build Server + run: cargo build --release --package easyremote-server --target x86_64-pc-windows-msvc + + - name: Prepare release + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path release/server + Copy-Item target/x86_64-pc-windows-msvc/release/easyremote-server.exe release/server/ + Copy-Item -Recurse crates/server/static release/server/ + Compress-Archive -Path release/server/* -DestinationPath easyremote-server-windows-x86_64.zip + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: easyremote-server-windows-x86_64 + path: easyremote-server-windows-x86_64.zip + + # 构建 Windows 客户端 (Tauri) + build-client-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: crates/client-tauri/ui/package-lock.json + + - name: Install frontend dependencies + working-directory: crates/client-tauri/ui + run: npm ci + + - name: Build frontend + working-directory: crates/client-tauri/ui + run: npm run build + + - name: Install Tauri CLI + run: cargo install tauri-cli + + - name: Build Tauri app + working-directory: crates/client-tauri + run: cargo tauri build + + - name: Upload MSI installer + uses: actions/upload-artifact@v4 + with: + name: easyremote-client-windows-x86_64-msi + path: target/release/bundle/msi/*.msi + + - name: Upload EXE + uses: actions/upload-artifact@v4 + with: + name: easyremote-client-windows-x86_64-exe + path: target/release/easyremote-client.exe + + # 创建 Release (仅在 tag 推送时) + create-release: + needs: [build-server-linux, build-server-windows, build-client-windows] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display structure + run: ls -R artifacts + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + artifacts/easyremote-server-linux-x86_64/*.tar.gz + artifacts/easyremote-server-windows-x86_64/*.zip + artifacts/easyremote-client-windows-x86_64-msi/*.msi + artifacts/easyremote-client-windows-x86_64-exe/*.exe + draft: false + prerelease: false + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile.server b/Dockerfile.server new file mode 100644 index 0000000..d23b632 --- /dev/null +++ b/Dockerfile.server @@ -0,0 +1,41 @@ +# EasyRemote Server Dockerfile +# 用于构建 Linux 版本的服务端 + +# 构建阶段 +FROM rust:1.75-bookworm as builder + +WORKDIR /app + +# 复制项目文件 +COPY . . + +# 构建服务端 +RUN cargo build --release --package easyremote-server + +# 运行阶段 +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# 从构建阶段复制可执行文件 +COPY --from=builder /app/target/release/easyremote-server . +COPY --from=builder /app/crates/server/static ./static + +# 创建数据目录 +RUN mkdir -p /app/data + +# 暴露端口 +EXPOSE 8080 +EXPOSE 3478/udp +EXPOSE 3479/udp + +# 环境变量 +ENV RUST_LOG=info +ENV DATABASE_URL=sqlite:///app/data/easyremote.db + +# 启动服务 +CMD ["./easyremote-server"] diff --git a/README.md b/README.md index f39b17b..eb30bbd 100644 --- a/README.md +++ b/README.md @@ -50,18 +50,60 @@ easyremote/ - Node.js 18+ (客户端前端) - SQLite (服务端数据库) -### 构建服务端 +### 构建服务端 (Windows) ```bash # 编译服务端 cargo build --release -p easyremote-server # 运行服务端 -./target/release/easyremote-server +.\target\release\easyremote-server.exe ``` 服务端默认运行在 `http://localhost:8080`,管理后台访问 `http://localhost:8080/` +### 构建服务端 (Linux) + +**方式一:直接在 Linux 上构建** +```bash +# 克隆项目 +git clone https://github.com/your-repo/easyremote.git +cd easyremote + +# 运行构建脚本 +chmod +x scripts/build-linux.sh +./scripts/build-linux.sh + +# 启动服务 +./release/linux/start.sh +``` + +**方式二:使用 Docker** +```bash +# 使用 docker-compose 一键部署 +docker-compose up -d + +# 或者手动构建和运行 +docker build -f Dockerfile.server -t easyremote-server . +docker run -d -p 8080:8080 -p 3478:3478/udp --name easyremote easyremote-server +``` + +**方式三:使用 systemd 服务** +```bash +# 复制文件到 /opt/easyremote +sudo mkdir -p /opt/easyremote +sudo cp -r release/linux/* /opt/easyremote/ + +# 安装 systemd 服务 +sudo cp /opt/easyremote/easyremote.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable easyremote +sudo systemctl start easyremote + +# 查看状态 +sudo systemctl status easyremote +``` + ### 构建客户端 ```bash diff --git a/crates/client-core/src/lib.rs b/crates/client-core/src/lib.rs index 3320437..31aa4b5 100644 --- a/crates/client-core/src/lib.rs +++ b/crates/client-core/src/lib.rs @@ -8,6 +8,6 @@ pub mod codec; pub mod config; pub use config::ClientConfig; -pub use signal::{SignalClient, SignalMessage}; +pub use signal::{SignalClient, SignalMessage, DisplayInfoMsg}; pub use capture::ScreenCapturer; pub use input::{InputController, MouseButton}; \ No newline at end of file diff --git a/crates/client-core/src/signal.rs b/crates/client-core/src/signal.rs index 55f4235..ec53bfe 100644 --- a/crates/client-core/src/signal.rs +++ b/crates/client-core/src/signal.rs @@ -9,6 +9,16 @@ use tokio::sync::{mpsc, RwLock}; use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +/// 显示器信息(用于消息传递) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DisplayInfoMsg { + pub index: usize, + pub width: u32, + pub height: u32, + pub is_primary: bool, + pub name: String, +} + /// 信令消息 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] @@ -118,6 +128,30 @@ pub enum SignalMessage { key: String, event_type: String, // "down", "up" }, + /// 获取显示器列表 + #[serde(rename = "get_displays")] + GetDisplays { + session_id: String, + from_device: String, + to_device: String, + }, + /// 显示器列表响应 + #[serde(rename = "displays_list")] + DisplaysList { + session_id: String, + from_device: String, + to_device: String, + displays: Vec, + current_display: usize, + }, + /// 切换显示器 + #[serde(rename = "switch_display")] + SwitchDisplay { + session_id: String, + from_device: String, + to_device: String, + display_index: usize, + }, /// 流媒体设置 #[serde(rename = "stream_settings")] StreamSettings { @@ -188,8 +222,15 @@ impl SignalClient { tokio::spawn(async move { while let Some(Ok(msg)) = read.next().await { if let Message::Text(text) = msg { - if let Ok(signal_msg) = serde_json::from_str::(&text) { - on_message(signal_msg); + println!("★★★ [客户端] 收到WebSocket消息: {}", &text[..text.len().min(200)]); + match serde_json::from_str::(&text) { + Ok(signal_msg) => { + println!("★★★ [客户端] 解析成功: {:?}", std::mem::discriminant(&signal_msg)); + on_message(signal_msg); + } + Err(e) => { + println!("★★★ [客户端] 解析失败: {}, 原始消息: {}", e, &text[..text.len().min(100)]); + } } } } diff --git a/crates/client-tauri/Cargo.toml b/crates/client-tauri/Cargo.toml index c581560..37fd166 100644 --- a/crates/client-tauri/Cargo.toml +++ b/crates/client-tauri/Cargo.toml @@ -45,6 +45,7 @@ anyhow = { workspace = true } thiserror = { workspace = true } dirs = "5.0" image = { workspace = true } +once_cell = "1.19" [features] default = ["custom-protocol"] diff --git a/crates/client-tauri/src/commands.rs b/crates/client-tauri/src/commands.rs index 5dbc673..5986821 100644 --- a/crates/client-tauri/src/commands.rs +++ b/crates/client-tauri/src/commands.rs @@ -18,7 +18,7 @@ static FORCE_OFFLINE_FLAG: AtomicBool = AtomicBool::new(false); /// 当前活跃的屏幕流会话 static ACTIVE_SESSION: tokio::sync::OnceCell>>> = tokio::sync::OnceCell::const_new(); -/// 流媒体设置 +/// 流媒体设置 (使用 std::sync::RwLock 以便在普通线程中使用) #[derive(Clone)] struct StreamSettings { resolution: f64, @@ -36,13 +36,29 @@ impl Default for StreamSettings { } } -/// 全局流媒体设置 -static STREAM_SETTINGS: tokio::sync::OnceCell>> = tokio::sync::OnceCell::const_new(); +/// 全局流媒体设置 (使用标准库的 RwLock) +use std::sync::RwLock as StdRwLock; +use once_cell::sync::Lazy; -async fn get_stream_settings() -> &'static Arc> { - STREAM_SETTINGS.get_or_init(|| async { - Arc::new(RwLock::new(StreamSettings::default())) - }).await +static STREAM_SETTINGS: Lazy>> = Lazy::new(|| { + Arc::new(StdRwLock::new(StreamSettings::default())) +}); + +fn get_stream_settings_sync() -> Arc> { + STREAM_SETTINGS.clone() +} + +/// 当前显示器索引 +static CURRENT_DISPLAY: Lazy> = Lazy::new(|| { + Arc::new(std::sync::atomic::AtomicUsize::new(0)) +}); + +fn get_current_display() -> usize { + CURRENT_DISPLAY.load(Ordering::SeqCst) +} + +fn set_current_display(index: usize) { + CURRENT_DISPLAY.store(index, Ordering::SeqCst); } /// 活跃屏幕会话 @@ -85,7 +101,8 @@ fn set_force_offline() { /// 启动屏幕流 async fn start_screen_streaming(session_id: String, controller_device: String, my_device_id: String) { - tracing::info!("启动屏幕流: session={}, controller={}", session_id, controller_device); + tracing::info!("★★★ 启动屏幕流: session={}, controller={}, my_device={}", session_id, controller_device, my_device_id); + println!("★★★ 启动屏幕流: session={}, controller={}, my_device={}", session_id, controller_device, my_device_id); // 检查是否已有活跃会话 let active_session = get_active_session().await; @@ -109,20 +126,42 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m // 获取信令客户端 let signal_client = get_signal_client().await; + // 获取 stop_flag 用于停止控制 + let stop_flag_clone = { + let session = active_session.read().await; + session.as_ref().map(|s| s.stop_flag.clone()).unwrap_or_else(|| Arc::new(AtomicBool::new(true))) + }; + // 启动屏幕捕获线程 let session_id_capture = session_id.clone(); let controller_device_capture = controller_device.clone(); - let my_device_id_capture = my_device_id.clone(); + let _my_device_id_capture = my_device_id.clone(); let signal_client_clone = signal_client.clone(); - let active_session_clone = active_session.clone(); - let stream_settings = get_stream_settings().await.clone(); + let stream_settings = get_stream_settings_sync(); - tokio::task::spawn_blocking(move || { - // 创建屏幕捕获器 - let mut capturer = match ScreenCapturer::new(0) { - Ok(c) => c, + std::thread::spawn(move || { + println!("★★★ 开始创建屏幕捕获器..."); + + // 创建一个 tokio runtime 用于发送帧 + let rt = match tokio::runtime::Runtime::new() { + Ok(r) => r, Err(e) => { + println!("★★★ 创建 tokio runtime 失败: {}", e); + return; + } + }; + + // 创建屏幕捕获器(使用当前选择的显示器) + let display_index = get_current_display(); + println!("★★★ 使用显示器: {}", display_index); + let mut capturer = match ScreenCapturer::new(display_index) { + Ok(c) => { + println!("★★★ 屏幕捕获器创建成功"); + c + } + Err(e) => { + println!("★★★ 创建屏幕捕获器失败: {}", e); tracing::error!("创建屏幕捕获器失败: {}", e); return; } @@ -130,6 +169,7 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m let width = capturer.width(); let height = capturer.height(); + println!("★★★ 屏幕分辨率: {}x{}", width, height); tracing::info!("屏幕分辨率: {}x{}", width, height); let mut frame_count = 0u64; @@ -138,24 +178,15 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m loop { // 获取当前设置 let (resolution, fps, _quality) = { - let rt = tokio::runtime::Handle::current(); - rt.block_on(async { - let s = stream_settings.read().await; - (s.resolution, s.fps, s.quality) - }) + let s = stream_settings.read().unwrap(); + (s.resolution, s.fps, s.quality) }; // 计算帧间隔 let frame_interval = std::time::Duration::from_millis(1000 / fps as u64); // 检查是否需要停止 - let should_stop = { - let rt = tokio::runtime::Handle::current(); - rt.block_on(async { - let session = active_session_clone.read().await; - session.as_ref().map(|s| s.stop_flag.load(Ordering::SeqCst)).unwrap_or(true) - }) - }; + let should_stop = stop_flag_clone.load(Ordering::SeqCst); if should_stop { tracing::info!("屏幕流停止"); @@ -180,12 +211,11 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m let jpeg_len = jpeg_data.len(); // 发送帧 - let rt = tokio::runtime::Handle::current(); let session_id_send = session_id_capture.clone(); let controller_device_send = controller_device_capture.clone(); let signal_client_send = signal_client_clone.clone(); - rt.block_on(async move { + rt.block_on(async { let client = signal_client_send.read().await; if let Some(c) = client.as_ref() { let _ = c.send_frame( @@ -201,9 +231,15 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m frame_count += 1; if frame_count % 30 == 0 { let actual_fps = frame_count as f64 / start_time.elapsed().as_secs_f64(); + println!("★★★ 屏幕流: {:.1} fps, 分辨率: {}x{}, 帧大小: {} KB", + actual_fps, scaled_width, scaled_height, jpeg_len / 1024); tracing::debug!("屏幕流: {:.1} fps, 分辨率: {}x{}, 帧大小: {} KB", actual_fps, scaled_width, scaled_height, jpeg_len / 1024); } + // 首帧日志 + if frame_count == 1 { + println!("★★★ 发送第一帧: {}x{}, {} 字节", scaled_width, scaled_height, jpeg_len); + } } } } @@ -220,12 +256,8 @@ async fn start_screen_streaming(session_id: String, controller_device: String, m std::thread::sleep(frame_interval); } - // 清理会话 - let rt = tokio::runtime::Handle::current(); - rt.block_on(async { - let mut session = active_session_clone.write().await; - *session = None; - }); + // 清理完成,stop_flag 已设置为 true + println!("★★★ 屏幕流线程结束"); }); } @@ -235,12 +267,9 @@ fn handle_mouse_input(x: f64, y: f64, event_type: &str, button: Option, delt // 获取当前分辨率设置以计算缩放比例 let scale = { - let rt = tokio::runtime::Handle::current(); - rt.block_on(async { - let settings = get_stream_settings().await; - let s = settings.read().await; - 1.0 / s.resolution - }) + let settings = get_stream_settings_sync(); + let s = settings.read().unwrap(); + 1.0 / s.resolution }; let actual_x = (x * scale) as i32; let actual_y = (y * scale) as i32; @@ -383,13 +412,17 @@ async fn connect_to_signal_server(device_id: String, server_url: String) -> Resu set_force_offline(); } SignalMessage::ConnectRequest { session_id, from_device, to_device, .. } => { - tracing::info!("收到连接请求: session_id={}, from={}, to={}", session_id, from_device, to_device); + tracing::info!("★★★ 收到连接请求: session_id={}, from={}, to={}", session_id, from_device, to_device); + println!("★★★ 收到连接请求: session_id={}, from={}, to={}", session_id, from_device, to_device); // 启动屏幕流 let session_id_clone = session_id.clone(); let from_device_clone = from_device.clone(); let device_id_stream = device_id_inner.clone(); - tokio::spawn(async move { - start_screen_streaming(session_id_clone, from_device_clone, device_id_stream).await; + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + start_screen_streaming(session_id_clone, from_device_clone, device_id_stream).await; + }); }); } SignalMessage::MouseEvent { x, y, event_type, button, delta, .. } => { @@ -403,12 +436,98 @@ async fn connect_to_signal_server(device_id: String, server_url: String) -> Resu SignalMessage::StreamSettings { resolution, fps, quality, .. } => { // 更新流媒体设置 tracing::info!("更新流媒体设置: resolution={}, fps={}, quality={}", resolution, fps, quality); + println!("★★★ 更新流媒体设置: resolution={}, fps={}, quality={}", resolution, fps, quality); + let settings = get_stream_settings_sync(); + let mut s = settings.write().unwrap(); + s.resolution = resolution; + s.fps = fps; + s.quality = quality; + } + SignalMessage::GetDisplays { session_id, from_device, .. } => { + // 获取显示器列表并返回 + println!("★★★ 收到获取显示器列表请求"); + use easyremote_client_core::capture::get_displays; + match get_displays() { + Ok(displays) => { + println!("★★★ 获取到 {} 个显示器", displays.len()); + for d in &displays { + println!(" - 显示器 {}: {}x{} (primary: {})", d.index, d.width, d.height, d.is_primary); + } + let display_msgs: Vec<_> = displays.iter().map(|d| { + easyremote_client_core::signal::DisplayInfoMsg { + index: d.index, + width: d.width, + height: d.height, + is_primary: d.is_primary, + name: format!("显示器 {} ({}x{})", d.index + 1, d.width, d.height), + } + }).collect(); + + let display_count = display_msgs.len(); + println!("★★★ 发送显示器列表响应: {} 个显示器", display_count); + + let response = SignalMessage::DisplaysList { + session_id, + from_device: device_id_inner.clone(), + to_device: from_device, + displays: display_msgs, + current_display: get_current_display(), + }; + let signal_client_holder = SIGNAL_CLIENT.get(); + if let Some(holder) = signal_client_holder { + let holder_clone = holder.clone(); + tokio::spawn(async move { + let client = holder_clone.read().await; + if let Some(c) = client.as_ref() { + let _ = c.send(response).await; + } + }); + } + } + Err(e) => { + println!("★★★ 获取显示器列表失败: {}", e); + } + } + } + SignalMessage::SwitchDisplay { display_index, from_device, session_id, .. } => { + // 切换显示器 + println!("★★★ 收到切换显示器请求: display_index={}", display_index); + set_current_display(display_index); + + // 停止当前屏幕流并重新启动 + let device_id_restart = device_id_inner.clone(); + let from_device_restart = from_device.clone(); + let session_id_restart = session_id.clone(); + tokio::spawn(async move { - let settings = get_stream_settings().await; - let mut s = settings.write().await; - s.resolution = resolution; - s.fps = fps; - s.quality = quality; + // 停止当前会话 + if let Some(holder) = ACTIVE_SESSION.get() { + let session = holder.read().await; + if let Some(s) = session.as_ref() { + s.stop_flag.store(true, Ordering::SeqCst); + } + } + + // 等待一会儿再重新启动 + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + start_screen_streaming(session_id_restart, from_device_restart, device_id_restart).await; + }); + } + SignalMessage::SessionEnd { session_id } => { + // 会话结束,停止屏幕流 + println!("★★★ 收到会话结束消息: session_id={}", session_id); + tracing::info!("收到会话结束消息: session_id={}", session_id); + + // 停止屏幕流 + tokio::spawn(async move { + if let Some(holder) = ACTIVE_SESSION.get() { + let mut session = holder.write().await; + if let Some(s) = session.as_ref() { + s.stop_flag.store(true, Ordering::SeqCst); + println!("★★★ 已停止屏幕流会话"); + } + *session = None; + } }); } _ => { @@ -519,7 +638,6 @@ pub async fn get_device_info(state: State<'_, AppStateHandle>) -> Result) -> Resu Ok(state.refresh_code()) } -/// 设置是否允许远程控制 -#[tauri::command] -pub async fn set_allow_remote(state: State<'_, AppStateHandle>, allow: bool) -> Result<(), String> { - let mut state = state.write().await; - state.allow_remote = allow; - Ok(()) -} - /// 用户登录 #[tauri::command] pub async fn login( @@ -740,9 +850,40 @@ pub async fn connect_to_device( /// 断开连接 #[tauri::command] pub async fn disconnect(state: State<'_, AppStateHandle>) -> Result<(), String> { - let mut state = state.write().await; - state.connection_state = ConnectionState::Disconnected; - state.current_session_id = None; + let mut app_state = state.write().await; + + // 获取当前会话信息 + let session_id = app_state.current_session_id.clone(); + let target_device = match &app_state.connection_state { + ConnectionState::Connected { target_device_id, .. } => Some(target_device_id.clone()), + _ => None, + }; + + // 清理状态 + app_state.connection_state = ConnectionState::Disconnected; + app_state.current_session_id = None; + drop(app_state); + + // 发送 SessionEnd 消息给被控端 + if let (Some(session_id), Some(target_device_id)) = (session_id, target_device) { + tracing::info!("发送 SessionEnd 到设备: {}", target_device_id); + let signal_client = get_signal_client().await; + let client = signal_client.read().await; + if let Some(c) = client.as_ref() { + let _ = c.end_session(session_id).await; + } + } + + // 停止本地屏幕流(如果是被控端) + if let Some(holder) = ACTIVE_SESSION.get() { + let mut session = holder.write().await; + if let Some(s) = session.as_ref() { + s.stop_flag.store(true, Ordering::SeqCst); + tracing::info!("已停止本地屏幕流"); + } + *session = None; + } + Ok(()) } @@ -819,6 +960,38 @@ pub async fn save_config( Ok(()) } +/// 重新连接服务器 +#[tauri::command] +pub async fn reconnect_server( + state: State<'_, AppStateHandle>, +) -> Result<(), String> { + let app_state = state.read().await; + let server_url = app_state.config.server_url.clone(); + let device_id = app_state.device_id.0.clone(); + drop(app_state); + + tracing::info!("重新连接服务器: {}", server_url); + + // 断开现有连接 + { + let signal_client = get_signal_client().await; + let mut client = signal_client.write().await; + if client.is_some() { + *client = None; + tracing::info!("已断开旧连接"); + } + } + + // 等待一下再重连 + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // 重新连接 + connect_to_signal_server(device_id, server_url).await?; + + tracing::info!("重新连接成功"); + Ok(()) +} + /// 获取开机启动状态 #[tauri::command] pub fn get_autostart() -> Result { diff --git a/crates/client-tauri/src/main.rs b/crates/client-tauri/src/main.rs index 292f1de..fd06d7d 100644 --- a/crates/client-tauri/src/main.rs +++ b/crates/client-tauri/src/main.rs @@ -212,7 +212,6 @@ fn main() { // 设备信息 commands::get_device_info, commands::refresh_verification_code, - commands::set_allow_remote, // 账号 commands::login, commands::logout, @@ -230,6 +229,7 @@ fn main() { // 配置 commands::get_config, commands::save_config, + commands::reconnect_server, // 开机启动 commands::get_autostart, commands::set_autostart, diff --git a/crates/client-tauri/src/state.rs b/crates/client-tauri/src/state.rs index 927d1c6..46eb348 100644 --- a/crates/client-tauri/src/state.rs +++ b/crates/client-tauri/src/state.rs @@ -13,8 +13,6 @@ pub struct AppState { pub device_id: DeviceId, /// 验证码 pub verification_code: VerificationCode, - /// 是否允许远程控制 - pub allow_remote: bool, /// 当前登录用户 pub current_user: Option, /// 认证令牌 @@ -73,7 +71,6 @@ impl AppState { config, device_id, verification_code, - allow_remote: false, current_user, auth_token, connection_state: ConnectionState::Disconnected, @@ -196,7 +193,6 @@ pub struct DeviceInfo { pub device_id: String, pub device_id_formatted: String, pub verification_code: String, - pub allow_remote: bool, pub device_name: String, pub os_type: String, } diff --git a/crates/client-tauri/tauri.conf.json b/crates/client-tauri/tauri.conf.json index 2a7b632..0c35531 100644 --- a/crates/client-tauri/tauri.conf.json +++ b/crates/client-tauri/tauri.conf.json @@ -51,7 +51,7 @@ "icons/icon.ico" ], "identifier": "com.easyremote.app", - "longDescription": "A remote desktop control application", + "longDescription": "简单易用的远程桌面控制软件", "macOS": { "entitlements": null, "exceptionDomain": "", @@ -60,12 +60,19 @@ "signingIdentity": null }, "resources": [], - "shortDescription": "Remote Desktop Control", + "shortDescription": "远程桌面控制", "targets": "all", "windows": { "certificateThumbprint": null, "digestAlgorithm": "sha256", - "timestampUrl": "" + "timestampUrl": "", + "wix": { + "language": "zh-CN" + }, + "nsis": { + "languages": ["SimpChinese"], + "displayLanguageSelector": false + } } }, "security": { diff --git a/crates/client-tauri/ui/src/App.vue b/crates/client-tauri/ui/src/App.vue index d9a9332..be6f1dd 100644 --- a/crates/client-tauri/ui/src/App.vue +++ b/crates/client-tauri/ui/src/App.vue @@ -16,7 +16,7 @@ const autostart = ref(false) // 配置 const config = ref({ - server_url: 'ws://localhost:8080', + server_url: 'ws://localhost:9099', device_name: '', quality: { frame_rate: 30, @@ -112,6 +112,7 @@ async function loadConfig() { async function saveConfig() { loading.value = true try { + const oldServerUrl = config.value.server_url const newConfig: ClientConfig = { ...config.value, server_url: configForm.value.server_url @@ -119,7 +120,21 @@ async function saveConfig() { await invoke('save_config', { config: newConfig }) config.value = newConfig configEditing.value = false - showSuccess('配置已保存,重启后生效') + + // 如果服务器地址改变了,自动重新连接 + if (oldServerUrl !== newConfig.server_url) { + showSuccess('配置已保存,正在重新连接服务器...') + try { + await invoke('reconnect_server') + showSuccess('已连接到新服务器') + // 刷新设备状态 + await loadDeviceInfo() + } catch (reconnectErr: any) { + error.value = '连接新服务器失败: ' + reconnectErr.toString() + } + } else { + showSuccess('配置已保存') + } } catch (e: any) { error.value = e.toString() } finally { @@ -159,17 +174,6 @@ async function refreshCode() { } } -async function toggleAllowRemote() { - if (!deviceInfo.value) return - try { - const newValue = !deviceInfo.value.allow_remote - await invoke('set_allow_remote', { allow: newValue }) - deviceInfo.value.allow_remote = newValue - } catch (e) { - console.error('设置失败:', e) - } -} - async function handleAuth() { loading.value = true error.value = '' @@ -397,15 +401,7 @@ function getDeviceIcon(osType: string): string {

本机设备

-

开启后,他人可通过本机ID和验证码远程协助您

-
-
- {{ deviceInfo?.allow_remote ? '已开启' : '已关闭' }} -
+

他人可通过本机ID和验证码远程协助您

@@ -702,7 +698,7 @@ function getDeviceIcon(osType: string): string { placeholder="ws://服务器IP:端口" v-model="configForm.server_url" /> -

示例: ws://192.168.1.100:8080 或 wss://example.com

+

示例: ws://192.168.1.100:9099 或 wss://example.com

diff --git a/crates/client-tauri/ui/src/types.ts b/crates/client-tauri/ui/src/types.ts index bf434c6..22683b4 100644 --- a/crates/client-tauri/ui/src/types.ts +++ b/crates/client-tauri/ui/src/types.ts @@ -4,7 +4,6 @@ export interface DeviceInfo { device_id: string; device_id_formatted: string; verification_code: string; - allow_remote: boolean; device_name: string; os_type: string; } @@ -22,7 +21,6 @@ export interface Device { os_type: string; os_version: string; is_online: boolean; - allow_remote: boolean; last_seen: string; } diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs index 1c1db0a..2550cef 100644 --- a/crates/server/src/config.rs +++ b/crates/server/src/config.rs @@ -126,7 +126,7 @@ impl Config { Ok(Self { host: std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()), port: std::env::var("PORT") - .unwrap_or_else(|_| "8080".to_string()) + .unwrap_or_else(|_| "9099".to_string()) .parse()?, stun_port, turn_port, diff --git a/crates/server/src/handlers/admin.rs b/crates/server/src/handlers/admin.rs index 231574a..28a5739 100644 --- a/crates/server/src/handlers/admin.rs +++ b/crates/server/src/handlers/admin.rs @@ -246,7 +246,7 @@ fn generate_default_env_config() -> String { # Server Settings HOST=0.0.0.0 -PORT=8080 +PORT=9099 # STUN Server Settings ENABLE_LOCAL_STUN=true diff --git a/crates/server/src/handlers/setup.rs b/crates/server/src/handlers/setup.rs index a318309..98469d4 100644 --- a/crates/server/src/handlers/setup.rs +++ b/crates/server/src/handlers/setup.rs @@ -26,12 +26,6 @@ pub struct SetupStatusResponse { pub struct SetupInitRequest { pub admin_username: String, pub admin_password: String, - pub jwt_secret: String, - pub jwt_expiry: i64, - pub stun_servers: String, - pub turn_server: Option, - pub turn_username: Option, - pub turn_password: Option, } /// 初始化响应 @@ -148,38 +142,42 @@ pub async fn init_setup( } /// 保存配置到 .env 文件 -fn save_env_config(req: &SetupInitRequest) { +fn save_env_config(_req: &SetupInitRequest) { + // 生成随机 JWT Secret + use std::time::{SystemTime, UNIX_EPOCH}; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let jwt_secret = format!("easyremote-jwt-secret-{}", timestamp); + let env_content = format!( - r#"# EasyRemote 服务端配置(自动生成) + r#"# EasyRemote Server Configuration (Auto Generated) -# 服务器配置 +# Server Settings HOST=0.0.0.0 -PORT=8080 +PORT=9099 -# 数据库配置 +# Database Settings DATABASE_URL=sqlite:easyremote.db?mode=rwc -# JWT 配置 +# JWT Settings JWT_SECRET={} -JWT_EXPIRY={} +JWT_EXPIRY=86400 -# STUN 服务器 -STUN_SERVERS={} +# STUN/TURN Settings +ENABLE_LOCAL_STUN=true +STUN_PORT=3478 +ENABLE_LOCAL_TURN=false +TURN_PORT=3479 +TURN_USERNAME=easyremote +TURN_PASSWORD=easyremote123 +TURN_REALM=easyremote.local -# TURN 服务器 -{} -{} -{} - -# 日志级别 +# Log Level RUST_LOG=info,tower_http=debug "#, - req.jwt_secret, - req.jwt_expiry, - req.stun_servers, - req.turn_server.as_ref().map(|s| format!("TURN_SERVER={}", s)).unwrap_or_else(|| "# TURN_SERVER=".to_string()), - req.turn_username.as_ref().map(|s| format!("TURN_USERNAME={}", s)).unwrap_or_else(|| "# TURN_USERNAME=".to_string()), - req.turn_password.as_ref().map(|s| format!("TURN_PASSWORD={}", s)).unwrap_or_else(|| "# TURN_PASSWORD=".to_string()), + jwt_secret ); // 保存到 .env 文件 diff --git a/crates/server/src/websocket.rs b/crates/server/src/websocket.rs index 4327fee..0c7f024 100644 --- a/crates/server/src/websocket.rs +++ b/crates/server/src/websocket.rs @@ -145,6 +145,40 @@ pub enum SignalMessage { fps: u32, quality: u32, }, + /// 获取显示器列表 + #[serde(rename = "get_displays")] + GetDisplays { + session_id: String, + from_device: String, + to_device: String, + }, + /// 显示器列表响应 + #[serde(rename = "displays_list")] + DisplaysList { + session_id: String, + from_device: String, + to_device: String, + displays: Vec, + current_display: usize, + }, + /// 切换显示器 + #[serde(rename = "switch_display")] + SwitchDisplay { + session_id: String, + from_device: String, + to_device: String, + display_index: usize, + }, +} + +/// 显示器信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DisplayInfoMsg { + pub index: usize, + pub width: u32, + pub height: u32, + pub is_primary: bool, + pub name: String, } /// 信令WebSocket处理器 @@ -367,6 +401,23 @@ async fn handle_signal_message(state: &Arc, device_id: &str, text: &st let _ = state.send_to_device(&to_device, text).await; } + // 转发获取显示器列表请求 + SignalMessage::GetDisplays { to_device, .. } => { + let _ = state.send_to_device(&to_device, text).await; + } + + // 转发显示器列表响应 + SignalMessage::DisplaysList { to_device, ref displays, .. } => { + tracing::info!("★★★ 转发显示器列表到 {}, 共 {} 个显示器", to_device, displays.len()); + let result = state.send_to_device(&to_device, text).await; + tracing::info!("★★★ 转发结果: {}", result); + } + + // 转发切换显示器请求 + SignalMessage::SwitchDisplay { to_device, .. } => { + let _ = state.send_to_device(&to_device, text).await; + } + _ => {} } } @@ -414,9 +465,18 @@ async fn handle_remote_socket(socket: WebSocket, state: Arc, device_id verification_code: String::new(), // 浏览器端需要管理员权限,跳过验证码 }; - let _ = state + // 打印当前所有在线设备 + { + let connections = state.connections.read().await; + let online_devices: Vec<_> = connections.keys().collect(); + tracing::info!("★★★ 当前在线设备列表: {:?}", online_devices); + tracing::info!("★★★ 尝试发送ConnectRequest到设备: {}", device_id); + } + + let send_result = state .send_to_device(&device_id, &serde_json::to_string(&connect_req).unwrap()) .await; + tracing::info!("★★★ 发送ConnectRequest结果: {}", send_result); // 发送任务 let send_task = tokio::spawn(async move { @@ -429,13 +489,24 @@ async fn handle_remote_socket(socket: WebSocket, state: Arc, device_id // 接收任务 let state_clone = state.clone(); - let _browser_id_clone = browser_device_id.clone(); + let browser_id_for_recv = browser_device_id.clone(); let target_device_id = device_id.clone(); let recv_task = tokio::spawn(async move { while let Some(Ok(msg)) = receiver.next().await { if let Message::Text(text) = msg { + // 替换 from_device 为正确的浏览器ID + let modified_text = if let Ok(mut json) = serde_json::from_str::(&text) { + if let Some(obj) = json.as_object_mut() { + if obj.get("from_device").map(|v| v.as_str()) == Some(Some("browser")) { + obj.insert("from_device".to_string(), serde_json::Value::String(browser_id_for_recv.clone())); + } + } + serde_json::to_string(&json).unwrap_or(text) + } else { + text + }; // 转发消息到目标设备 - let _ = state_clone.send_to_device(&target_device_id, &text).await; + let _ = state_clone.send_to_device(&target_device_id, &modified_text).await; } } }); @@ -445,6 +516,13 @@ async fn handle_remote_socket(socket: WebSocket, state: Arc, device_id _ = recv_task => {}, } + // 发送 SessionEnd 消息给客户端,停止屏幕流 + let session_end = SignalMessage::SessionEnd { + session_id: session_id.clone(), + }; + let _ = state.send_to_device(&device_id, &serde_json::to_string(&session_end).unwrap()).await; + tracing::info!("★★★ 浏览器断开连接,发送SessionEnd到设备: {}", device_id); + // 清理 state.remove_connection(&browser_device_id).await; } diff --git a/crates/server/static/index.html b/crates/server/static/index.html index eccaaa4..973867b 100644 --- a/crates/server/static/index.html +++ b/crates/server/static/index.html @@ -6,18 +6,56 @@ EasyRemote 管理后台 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/release/windows-client/EasyRemote_0.1.0_x64_zh-CN.msi b/release/windows-client/EasyRemote_0.1.0_x64_zh-CN.msi new file mode 100644 index 0000000..4989eff Binary files /dev/null and b/release/windows-client/EasyRemote_0.1.0_x64_zh-CN.msi differ diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100644 index 0000000..6848446 --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# EasyRemote Server Linux 构建脚本 +# 在 Linux 服务器上运行此脚本来构建服务端 + +set -e + +echo "=====================================" +echo " EasyRemote Server Linux Build" +echo "=====================================" + +# 检查 Rust 是否安装 +if ! command -v cargo &> /dev/null; then + echo "Rust 未安装,正在安装..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source $HOME/.cargo/env +fi + +echo "Rust 版本: $(rustc --version)" +echo "Cargo 版本: $(cargo --version)" + +# 构建服务端 +echo "" +echo "正在构建服务端..." +cargo build --release --package easyremote-server + +# 创建发布目录 +RELEASE_DIR="./release/linux" +mkdir -p "$RELEASE_DIR" + +# 复制文件 +cp target/release/easyremote-server "$RELEASE_DIR/" +cp -r crates/server/static "$RELEASE_DIR/" + +# 创建启动脚本 +cat > "$RELEASE_DIR/start.sh" << 'EOF' +#!/bin/bash +cd "$(dirname "$0")" +./easyremote-server +EOF +chmod +x "$RELEASE_DIR/start.sh" + +# 创建 systemd 服务文件 +cat > "$RELEASE_DIR/easyremote.service" << 'EOF' +[Unit] +Description=EasyRemote Server +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/easyremote +ExecStart=/opt/easyremote/easyremote-server +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +echo "" +echo "=====================================" +echo " 构建完成!" +echo "=====================================" +echo "" +echo "发布文件位于: $RELEASE_DIR/" +echo "" +echo "文件列表:" +ls -la "$RELEASE_DIR/" +echo "" +echo "部署步骤:" +echo "1. 复制 release/linux/ 目录到服务器 /opt/easyremote/" +echo "2. 复制 easyremote.service 到 /etc/systemd/system/" +echo "3. 运行: systemctl daemon-reload" +echo "4. 运行: systemctl enable easyremote" +echo "5. 运行: systemctl start easyremote" +echo "" +echo "或者直接运行: ./start.sh" diff --git a/static/index.html b/static/index.html index eccaaa4..08cecab 100644 --- a/static/index.html +++ b/static/index.html @@ -6,18 +6,56 @@ EasyRemote 管理后台