全部文章

<h1>stm32-demo</h1> <h2>一、前置准备</h2> <table> <thead> <tr> <th>项</th> <th>备注</th> </tr> </thead> <tbody> <tr> <td>操作系统</td> <td>Windows 11 25H2</td> </tr> <tr> <td>编辑器</td> <td>Zed</td> </tr> <tr> <td>目标芯片</td> <td>STM32F103C8T6</td> </tr> <tr> <td>仿真器</td> <td>CMSIS-DAP</td> </tr> </tbody> </table> <p>开始之前请确保你的电脑已经安装了:<code>Rust</code> 和 <code>arm-none-eabi-gcc</code></p> <h3>1.1 安装 <code>cargo-binutils</code></h3> <pre><code class="language-cmd">rustup component add llvm-tools cargo install cargo-binutils</code></pre> <h3>1.2 安装 <code>cargo-embed</code></h3> <pre><code class="language-cmd">cargo install probe-rs-tools</code></pre> <h3>1.3 安装 <code>嵌入式编译工具链</code></h3> <pre><code class="language-cmd">rustup target add thumbv7m-none-eabi</code></pre> <h2>二、拉取代码</h2> <p><code>git clone git@github.com:pcdeng/stm32-demo.git</code></p> <h2>四、接上调试器</h2> <p><img src="/uploads/link.jpeg" alt="" /></p> <h2>编译并烧录</h2> <pre><code class="language-cmd">cargo embed</code></pre> <p>正常的话,会输出</p> <pre><code class="language-cmd"> Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s Profile default Target D:\projects\rust\stm32-demo\target\thumbv7m-none-eabi\debug\stm32demo Erasing ⠁ 0% [--------------------] Erasing ✔ 100% [####################] 28.00 KiB @ 11.04 KiB/s (took 3s) Programming ✔ 100% [####################] 28.00 KiB @ 7.50 KiB/s (took 4s) Finished in 6.28s Done processing config profile default</code></pre> <h2>验证</h2> <p>如果一切正常 <code>PC13指示灯</code>,间隔 1s 闪烁。</p> <p><img src="/uploads/rust-embed-led.jpg" alt="" /></p> <h1>参考资料</h1> <p><a href="https://www.rust-lang.org/zh-CN/tools/install">安装 Rust</a></p> <p><a href="https://jzow.github.io/discovery/microbit/03-setup/windows.html">安装 arm-none-eabi-gcc</a></p> <p><a href="https://detail.tmall.com/item.htm?_u=3ko4pss5f42\&id=631928588828\&spm=a1z09.2.0.0.d5592e8dy0qIbk">我用的开发板</a></p> <p><a href="https://jzow.github.io/discovery/microbit/index.html">Discovery</a></p> <p><a href="https://github.com/stm32-rs/stm32f1xx-hal">stm32f1xx-hal</a></p> <p><a href="https://space.bilibili.com/500416539/channel/collectiondetail?sid=177577\&ctype=0">Rust嵌入式开发入门</a> 强烈推荐,作者解说得非常详细。</p> <p><a href="https://probe.rs/docs/tools/cargo-embed/">cargo embed 配置参考</a></p>
详情
<p>继上一篇 <a href="https://juejin.cn/post/7605150769009164331">基于 GPUI 实现 WebSocket 服务端之 UI 篇</a>, 这篇聚焦于如何实现 WebSocket 服务和 GPUI 的交互。</p> <h1>解耦</h1> <p>通过 <code>event bus</code> 来解耦 <code>UI</code> 和 <code>服务</code></p> <h2>交互流程图</h2> <p>简单地说:</p> <ol> <li><code>UI</code> 向<code>服务</code>发送<code>命令</code> <code>send_command</code>,<code>服务</code>接收到<code>命令</code>然后做对应的处理。</li> <li><code>服务</code>向 <code>UI</code> 发送<code>事件</code> <code>emit_event</code>,<code>UI</code> 接收到<code>事件</code>然后做对应的处理(更新 UI)</li> </ol> <p><img src="/uploads/event-bus.png" alt="" /></p> <h1>目录结构</h1> <pre><code class="language-bash">├─src │ event_bus.rs │ lib.rs │ log.rs │ main.rs │ ws_server.rs │ ws_ui.rs │</code></pre> <h2>event_bus.rs</h2> <pre><code class="language-rust">use tokio::sync::mpsc; #[derive(Clone, Debug)] pub struct ClientInfo { pub id: String, pub addr: String, } #[derive(Clone, Debug)] pub enum ServerCommand { Start, Stop, SendToClient { client_id: String, message: String }, DisconnectClient { client_id: String }, Broadcast { message: String }, } #[derive(Clone, Debug)] pub enum ServerEvent { Started, Stopped, Error(String), } #[derive(Clone)] pub struct EventBus { pub command_tx: mpsc::UnboundedSender<ServerCommand>, pub event_tx: mpsc::UnboundedSender<ServerEvent>, } impl EventBus { pub fn new() -> ( Self, mpsc::UnboundedReceiver<ServerEvent>, mpsc::UnboundedReceiver<ServerCommand>, ) { let (event_tx, event_rx) = mpsc::unbounded_channel(); let (command_tx, command_rx) = mpsc::unbounded_channel(); ( Self { event_tx, command_tx, }, event_rx, command_rx, ) } pub fn emit_event(&self, event: ServerEvent) { let _ = self.event_tx.send(event); } pub fn send_command(&self, cmd: ServerCommand) { let _ = self.command_tx.send(cmd); } }</code></pre> <h2>lib.rs</h2> <p>导出各个模块</p> <pre><code class="language-rust">pub mod event_bus; pub mod log; pub mod ws_server; pub mod ws_ui;</code></pre> <h2>log.rs</h2> <p>自定义日志格式</p> <pre><code class="language-rust">use std::fs::OpenOptions; use log::LevelFilter; use simplelog::{ ColorChoice, CombinedLogger, Config, ConfigBuilder, TermLogger, TerminalMode, WriteLogger, format_description, }; pub fn init_logger(log_file_name: &str) { let mut config = ConfigBuilder::new(); config.set_time_format_custom(format_description!( "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]" )); CombinedLogger::init(vec![ TermLogger::new( LevelFilter::Warn, Config::default(), TerminalMode::Mixed, ColorChoice::Auto, ), WriteLogger::new( LevelFilter::Info, config.build(), OpenOptions::new() .create(true) .append(true) .open(log_file_name) .unwrap(), ), ]) .unwrap(); }</code></pre> <h2>main.rs</h2> <p>程序入口</p> <pre><code class="language-rust">#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use gpui::{App, AppContext, Application, Bounds, WindowBounds, WindowOptions, px, size}; use gpui_tokio_bridge::{Tokio, init}; use ws_gpui::{ event_bus::EventBus, log::init_logger, ws_server::WebSocketServer, ws_ui::WebSocketUi, }; fn on_finish_launching(cx: &mut App) { init(cx); // 非常重要,留到后面介绍。 let (event_bus, mut event_rx, command_rx) = EventBus::new(); // 创建 WebSocket 服务 let ws_service = WebSocketServer::new(event_bus.clone()); let bounds = Bounds::centered(None, size(px(500.), px(250.0)), cx); let window_handle = cx .open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, move |_win, cx| { cx.new(|cx| { // 在 tokio 任务处理来自 UI 的命令 Tokio::spawn(cx, async move { ws_service.start(command_rx).await; }) .detach(); WebSocketUi::new(event_bus) }) }, ) .unwrap(); // 在独立的任务处理来自服务的 事件 cx.spawn(async move |async_cx| { while let Some(event) = event_rx.recv().await { let event_clone = event.clone(); // 异步更新 UI,避免阻塞 let _ = window_handle.update(async_cx, |client, _win, cx| { client.handle_event(event_clone, cx); }); } }) .detach(); } fn main() { init_logger("ws-gui.log"); // 初始化日志 let app = Application::new(); app.run(on_finish_launching); }</code></pre> <h2>ws_server.rs</h2> <p>基于 <code>tokio_tungstenite</code> 实现 ws 服务,写得不够简洁,有时间可以再优化一下。</p> <pre><code class="language-rust">use crate::event_bus::{ClientInfo, EventBus, ServerCommand, ServerEvent}; use futures_util::{SinkExt, StreamExt}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{RwLock, broadcast, mpsc}; use tokio_tungstenite::accept_async; use uuid::Uuid; type ClientSender = mpsc::UnboundedSender<String>; pub struct WebSocketServer { clients: Arc<RwLock<HashMap<String, ClientSender>>>, event_bus: EventBus, } impl WebSocketServer { pub fn new(event_bus: EventBus) -> Self { Self { clients: Arc::new(RwLock::new(HashMap::new())), event_bus, } } // 新增一个带关闭信号的客户端处理函数 async fn handle_client_with_shutdown( stream: tokio::net::TcpStream, addr: std::net::SocketAddr, clients: Arc<RwLock<HashMap<String, ClientSender>>>, event_tx: mpsc::UnboundedSender<ServerEvent>, mut shutdown_rx: broadcast::Receiver<()>, ) { let client_id = Uuid::new_v4().to_string(); let client_info = ClientInfo { id: client_id.clone(), addr: addr.to_string(), }; let ws_stream = match accept_async(stream).await { Ok(ws) => ws, Err(e) => { println!("WebSocket 握手失败: {}", e); return; } }; let (mut ws_sender, mut ws_receiver) = ws_stream.split(); let (tx, mut rx) = mpsc::unbounded_channel::<String>(); clients.write().await.insert(client_id.clone(), tx); let _ = event_tx.send(ServerEvent::ClientConnected(client_info)); let client_id_for_log = client_id.clone(); // 使用原子布尔值来标记服务器是否正在关闭 let server_is_shutting_down = Arc::new(std::sync::atomic::AtomicBool::new(false)); // 启动消息发送任务,同时监听服务器关闭信号 let server_is_shutting_down_clone = server_is_shutting_down.clone(); tokio::spawn(async move { loop { tokio::select! { msg = rx.recv() => { match msg { Some(msg_content) => { // 检查是否是特殊的关闭消息 if msg_content == "CLOSE" { if ws_sender .send(tokio_tungstenite::tungstenite::Message::Close(None)) .await .is_err() { break; } } else { if ws_sender .send(tokio_tungstenite::tungstenite::Message::Text(msg_content.into())) .await .is_err() { break; } } } None => break, // 通道已关闭 } } res = shutdown_rx.recv() => { // 检查是否收到关闭信号或发送方已关闭 match res { Ok(()) => { // 收到服务器关闭信号,设置关闭标志 server_is_shutting_down_clone.store(true, std::sync::atomic::Ordering::Relaxed); // 发送关闭消息并退出 if ws_sender .send(tokio_tungstenite::tungstenite::Message::Close(None)) .await .is_ok() { // 等待一小段时间让消息发送完成 tokio::time::sleep(std::time::Duration::from_millis(100)).await; } } Err(broadcast::error::RecvError::Closed) => { // 发送方已关闭,同样需要设置关闭标志 server_is_shutting_down_clone.store(true, std::sync::atomic::Ordering::Relaxed); if ws_sender .send(tokio_tungstenite::tungstenite::Message::Close(None)) .await .is_ok() { // 等待一小段时间让消息发送完成 tokio::time::sleep(std::time::Duration::from_millis(100)).await; } } Err(broadcast::error::RecvError::Lagged(_)) => { // 一些消息滞后了,但我们仍然认为应该设置关闭标志 server_is_shutting_down_clone.store(true, std::sync::atomic::Ordering::Relaxed); if ws_sender .send(tokio_tungstenite::tungstenite::Message::Close(None)) .await .is_ok() { // 等待一小段时间让消息发送完成 tokio::time::sleep(std::time::Duration::from_millis(100)).await; } } } break; } } } }); while let Some(msg) = ws_receiver.next().await { match msg { Ok(tokio_tungstenite::tungstenite::Message::Text(text)) => { println!("收到来自 {} 的消息: {}", client_id_for_log, text); } Ok(tokio_tungstenite::tungstenite::Message::Close(_)) => { println!("客户端 {} 关闭连接", client_id_for_log); break; } Err(_) => { break; } _ => {} } } // 检查服务器是否正在关闭,如果是,则不发送断开事件 let is_server_shutdown = server_is_shutting_down.load(std::sync::atomic::Ordering::Relaxed); clients.write().await.remove(&client_id); if !is_server_shutdown { let _ = event_tx.send(ServerEvent::ClientDisconnected(client_id)); } } pub async fn start(&self, mut command_rx: mpsc::UnboundedReceiver<ServerCommand>) { let mut server_handle: Option<tokio::task::JoinHandle<()>> = None; let mut shutdown_tx: Option<tokio::sync::broadcast::Sender<()>> = None; let clients = self.clients.clone(); let event_bus = self.event_bus.clone(); // 主命令处理循环 while let Some(cmd) = command_rx.recv().await { match cmd { ServerCommand::Start => { if server_handle.is_none() { // 创建停止通道 - 使用 broadcast 通道,可以被多个客户端订阅 let (server_shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(100); shutdown_tx = Some(server_shutdown_tx.clone()); // 克隆必要的变量供异步任务使用 let event_bus_clone = event_bus.clone(); let clients_clone = clients.clone(); // 启动服务器任务 server_handle = Some(tokio::spawn(async move { // 绑定TCP监听器 let listener = match tokio::net::TcpListener::bind("127.0.0.1:9001") .await { Ok(l) => l, Err(e) => { event_bus_clone .emit_event(ServerEvent::Error(format!("启动失败: {}", e))); println!("绑定地址失败: {}", e); return; } }; event_bus_clone.emit_event(ServerEvent::Started); println!("WebSocket 服务器已启动在 127.0.0.1:9001"); let mut server_shutdown_rx = server_shutdown_tx.subscribe(); // 为服务器主线程创建一个订阅 loop { tokio::select! { // 接受新连接 accept_result = listener.accept() => { match accept_result { Ok((stream, addr)) => { let clients = clients_clone.clone(); let event_tx = event_bus_clone.event_tx.clone(); let shutdown_rx = server_shutdown_tx.subscribe(); // 为每个客户端创建一个订阅 tokio::spawn(async move { WebSocketServer::handle_client_with_shutdown(stream, addr, clients, event_tx, shutdown_rx).await; }); } Err(e) => { println!("接受连接失败: {}", e); } } } // 监听停止信号 _ = server_shutdown_rx.recv() => { println!("服务器停止信号已接收"); break; } } } // 服务器停止时主动断开所有客户端连接 - 异步处理以避免阻塞 let clients_clone_for_cleanup = clients_clone.clone(); tokio::spawn(async move { let clients_map = clients_clone.read().await; let client_ids: Vec<String> = clients_map.keys().cloned().collect(); drop(clients_map); // 释放读锁 // 并发向所有客户端发送断开连接消息 let mut handles = Vec::new(); for client_id in client_ids { let clients = clients_clone.clone(); let handle = tokio::spawn(async move { let clients = clients.read().await; if let Some(tx) = clients.get(&client_id) { let _ = tx.send("CLOSE".to_string()); // 特殊消息表示关闭 } drop(clients); }); handles.push(handle); } // 等待所有发送任务完成 for handle in handles { let _ = handle.await; } // 清理客户端列表 { let mut clients = clients_clone_for_cleanup.write().await; clients.clear(); } }); // 添加短暂延迟以确保清理完成 tokio::time::sleep(std::time::Duration::from_millis(100)).await; // 发送停止事件 event_bus_clone.emit_event(ServerEvent::Stopped); println!("WebSocket 服务器已完全停止"); })); } } ServerCommand::Stop => { if let Some(handle) = server_handle.take() { if let Some(tx) = shutdown_tx.take() { let _ = tx.send(()); // 发送停止信号给所有订阅者 } // 异步等待服务器任务结束,避免阻塞UI线程 tokio::spawn(async move { let _ = handle.await; }); } else { // 如果服务器没有运行,也要发送停止事件 event_bus.emit_event(ServerEvent::Stopped); } } ServerCommand::Broadcast { message } => { if server_handle.is_some() { // 仅当服务器正在运行时才处理命令 let clients = clients.clone(); let msg = message.clone(); tokio::spawn(async move { let clients = clients.read().await; for tx in clients.values() { let _ = tx.send(msg.clone()); } }); } } } } } } </code></pre> <h2>ws_ui.rs</h2> <p>UI 部分,完整代码可以参考 <a href="https://juejin.cn/post/7605150769009164331">基于 GPUI 实现 WebSocket 服务端之 UI 篇</a></p> <pre><code class="language-rust">use gpui::{ClickEvent, CursorStyle, Window, div, prelude::*, rgb}; use crate::event_bus::{ClientInfo, EventBus, ServerCommand, ServerEvent}; pub struct WebSocketUi { is_running: bool, event_bus: EventBus, clients: Vec<ClientInfo>, } impl WebSocketUi { pub fn new(event_bus: EventBus) -> Self { Self { is_running: false, event_bus, clients: vec![], } } fn start( self: &mut WebSocketUi, _evt: &ClickEvent, _win: &mut Window, _cx: &mut Context<Self>, ) { if !self.is_running { self.event_bus.send_command(ServerCommand::Start); } } fn disconnect( self: &mut WebSocketUi, _evt: &ClickEvent, _win: &mut Window, _cx: &mut Context<Self>, ) { if self.is_running { self.event_bus.send_command(ServerCommand::Stop); } } fn send_test_message( self: &mut WebSocketUi, _evt: &ClickEvent, _win: &mut Window, _cx: &mut Context<Self>, ) { if self.is_running { let message = "Hello, World from ws server!".into(); self.event_bus .send_command(ServerCommand::Broadcast { message }); } } ... 省略一些代码 pub fn handle_event(&mut self, event: ServerEvent, cx: &mut Context<Self>) { match event { ServerEvent::Started => { self.is_running = true; } ServerEvent::Stopped => { self.is_running = false; self.clients.clear(); } ServerEvent::ClientConnected(info) => { self.clients.push(info); } ServerEvent::ClientDisconnected(id) => { self.clients.retain(|c| c.id != id); } ServerEvent::Error(msg) => { println!("错误: {}", msg); } } cx.notify(); } } ...省略一些代码</code></pre> <h1>踩的坑</h1> <h2>UI 没有响应</h2> <p>现象:此时,UI 已经没有响应。</p> <p><img src="/uploads/%E6%B2%A1%E6%9C%89%E5%93%8D%E5%BA%94.png" alt="" /></p> <p><img src="/uploads/%E6%89%B9%E9%87%8F%E8%BF%9E%E6%8E%A5%E6%B5%8B%E8%AF%95.png" alt="" /> 根本原因是:存在两个异步运行时。</p> <p>解决方法:引入 <a href="https://crates.io/crates/gpui-tokio-bridge">gpui-tokio-bridge</a>,参考 <a href="https://github.com/zed-industries/zed/tree/main/crates/gpui_tokio">gpui_tokio</a></p> <p>复现:</p> <p><img src="/uploads/%E5%A4%8D%E7%8E%B0%20UI%20%E6%97%A0%E5%93%8D%E5%BA%94%E4%BB%A3%E7%A0%81.png" alt="" /></p> <h1>测试</h1> <h2>网页测试</h2> <p><img src="/uploads/ws_test.gif" alt="" /></p> <h1>参考</h1> <ul> <li><a href="https://crates.io/crates/gpui-tokio-bridge">gpui-tokio-bridge</a></li> <li><a href="https://www.gpui.rs/">gpui</a></li> <li><a href="https://crates.io/crates/tokio-tungstenite">tokio_tungstenite</a></li> </ul>
详情
<h1>一、概述</h1> <p>最近在学习 Rust 和 gpui,基于我学到的和遇到的问题,总结了一下,也顺便记录一下。</p> <h2>1、创建项目并用 Zed 打开</h2> <pre><code class="language-cmd">cargo new ws-gpui zed ws-gpui</code></pre> <h2>2、安装依赖</h2> <pre><code class="language-cmd">cargo add gpui</code></pre> <h2>3、创建一个空窗口</h2> <pre><code class="language-rust">use gpui::{ App, Application, Bounds, Empty, Entity, Window, WindowBounds, WindowOptions, prelude::*, px, size, }; struct WebSocketUi {} impl WebSocketUi { fn new() -> Self { Self {} } } impl Render for WebSocketUi { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { Empty {} } } fn build_root_view(_window: &mut Window, cx: &mut App) -> Entity<WebSocketUi> { cx.new(|_cx| WebSocketUi::new()) } fn on_finish_launching(cx: &mut App) { let bounds = Bounds::centered(None, size(px(500.), px(250.0)), cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, build_root_view, ) .unwrap(); } fn main() { let app = Application::new(); app.run(on_finish_launching); }</code></pre> <h1>二、拆开来看看</h1> <h2>1、创建 app 并运行</h2> <pre><code class="language-rust">fn main() { let app = Application::new(); app.run(on_finish_launching); }</code></pre> <h2>2、app 运行后,创建一个窗口。</h2> <pre><code class="language-rust">fn on_finish_launching(cx: &mut App) { let bounds = Bounds::centered(None, size(px(500.), px(250.0)), cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, build_root_view, ) .unwrap(); }</code></pre> <p>创建一个在主显示器居中的,500px * 250px 的窗口,窗口的其它选项用默认的。</p> <h2>3、窗口打开后,实例化一个实体(Entity), T -> WebSocketUi</h2> <pre><code class="language-rust">fn build_root_view(_window: &mut Window, cx: &mut App) -> Entity<WebSocketUi> { cx.new(|_cx| WebSocketUi::new()) }</code></pre> <h2>4、WebSocketUi 需要实现 Render</h2> <pre><code class="language-rust">impl Render for WebSocketUi { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { Empty {} } }</code></pre> <p>render 方法现在是返回一个 <code>Empty</code> ,所以窗口里面什么内容都没有。</p> <h2>5、运行看看效果。</h2> <pre><code class="language-cmd">cargo run</code></pre> <h2>6、效果</h2> <p><img src="/uploads/1.png" alt="" /></p> <h1>三、实现业务需要的 UI</h1> <h2>1、创建一个简单示例。</h2> <h3>1.1、创建背景和示例文字</h3> <p>需要创建一个内容撑满窗口的白色背景的内边距是 16px 的 UI。</p> <pre><code class="language-rust">impl Render for WebSocketUi { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { div() .size_full() .p_4() .bg(rgb(0xffffff)) .child("Hello gpui") } }</code></pre> <p>如果你熟悉 <code>Tailwind CSS</code>,你应该对这种语法很熟悉。请参考 <a href="https://tailwindcss.com/docs/styling-with-utility-classes">Tailwind CSS</a></p> <h3>1.2、看看效果</h3> <p><img src="/uploads/ws_gpui_empty.png" alt="" /></p> <h2>2、创建整体结构</h2> <h3>2.1、代码实现</h3> <pre><code class="language-rust">impl Render for WebSocketUi { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { div().size_full().p_4().bg(rgb(0xffffff)).child( div() .flex() .flex_col() .gap_4() .size_full() .justify_center() .items_center() .child("服务运行状态栏在这里") .child("状态信息栏在这里") .child("操作栏在这里"), ) } }</code></pre> <p>这种代码对前端开发来说特别<code>亲切</code>。</p> <h3>2.2、看看效果</h3> <p><img src="/uploads/ws_gpui_layout.png" alt="" /></p> <h2>3、实现服务运行状态栏。</h2> <h3>3.1、代码实现</h3> <pre><code class="language-rust">struct WebSocketUi { is_running: bool, // 组件状态,和 React 的 const [isRunning, setIsRunning] = useState(false) 有点像 } impl WebSocketUi { fn new() -> Self { Self { is_running: false } } fn status_bar(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { let (connection_status, status_color) = if self.is_running { ("已开启", rgb(0x00ff00)) } else { ("未开启", rgb(0xff0000)) }; div() .flex() .items_center() .gap_2() .child("服务状态:") .child(div().w_4().h_4().rounded_full().bg(status_color)) .child(connection_status) } } impl Render for WebSocketUi { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { div().size_full().p_4().bg(rgb(0xffffff)).child( div() .flex() .flex_col() .gap_4() .size_full() .justify_center() .items_center() .child(self.status_bar(window, cx)) .child("状态信息栏在这里") .child("操作栏在这里"), ) } }</code></pre> <h3>3.2、看看效果</h3> <p><img src="/uploads/ws_gpui_status_bar.png" alt="" /></p> <h2>4、实现状态信息栏</h2> <h3>4.1、代码实现</h3> <pre><code class="language-rust">struct WebSocketUi { is_running: bool, status_message: String, } impl WebSocketUi { fn new() -> Self { Self { is_running: false, status_message: "未开启".into(), } } // 省略了 status_bar 方法 } impl Render for WebSocketUi { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let status_message = self.status_message.clone(); div().size_full().p_4().bg(rgb(0xffffff)).child( div() // 省略了一些代码 .child(status_message) .child("操作栏在这里"), ) } }</code></pre> <h3>4.2、看看效果</h3> <p><img src="/uploads/ws_gpui_message_bar.png" alt="" /></p> <h2>5、实现操作栏</h2> <h3>5.1、代码实现</h3> <pre><code class="language-rust">impl WebSocketUi { fn start(this: &mut WebSocketUi, _evt: &ClickEvent, _win: &mut Window, cx: &mut Context<Self>) { if !this.is_running { this.is_running = true; this.status_message = "已连接".into(); cx.notify(); } } fn disconnect( this: &mut WebSocketUi, _evt: &ClickEvent, _win: &mut Window, cx: &mut Context<Self>, ) { if this.is_running { this.is_running = false; this.status_message = "未连接".into(); cx.notify(); } } fn send_test_message( this: &mut WebSocketUi, _evt: &ClickEvent, _win: &mut Window, cx: &mut Context<Self>, ) { if this.is_running { this.status_message = "消息已发送".into(); cx.notify(); } } fn actions_bar(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let (start_cursor, start_bg, stop_cursor, stop_bg) = if self.is_running { ( CursorStyle::OperationNotAllowed, gpui::black().opacity(0.4), CursorStyle::PointingHand, gpui::black(), ) } else { ( CursorStyle::PointingHand, gpui::black(), CursorStyle::OperationNotAllowed, gpui::black().opacity(0.4), ) }; div() .flex() .gap_2() .child( div() .id("connect") // 这里要注意,和用户交互的 div 必须要加上 id,比如点击、滚动 .child("开启") .text_color(gpui::white()) .bg(start_bg) .rounded_md() .py_0p5() .px_1() .cursor(start_cursor) .on_click(cx.listener(Self::start)), ) .child( div() .id("disconnect") .child("关闭") .text_color(gpui::white()) .bg(stop_bg) .rounded_md() .py_0p5() .px_1() .cursor(stop_cursor) .on_click(cx.listener(Self::disconnect)), ) .child( div() .id("send") .child("发送测试消息") .text_color(gpui::white()) .bg(stop_bg) .rounded_md() .py_0p5() .px_1() .cursor(stop_cursor) .on_click(cx.listener(Self::send_test_message)), ) } } impl Render for WebSocketUi { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { div().size_full().p_4().bg(rgb(0xffffff)).child( div() // 省略了一些代码 .child(self.actions_bar(window, cx)), ) } }</code></pre> <h3>5.2、看看效果</h3> <p><img src="/uploads/ws_gpui_action_bar.png" alt="" /></p> <h1>四、总结</h1> <h2>1、分清楚不同的 Context(cx)</h2> <table> <thead> <tr> <th>上下文类型</th> <th>线程</th> <th>主要用途</th> </tr> </thead> <tbody> <tr> <td>AppContext</td> <td>主线程</td> <td>应用启动、打开窗口</td> </tr> <tr> <td>Context\<T></td> <td>主线程</td> <td>组件逻辑、UI 更新</td> </tr> </tbody> </table> <h2>2、熟悉 <a href="https://tailwindcss.com/docs/styling-with-utility-classes">Tailwind CSS</a></h2> <h2>3、熟悉 <a href="https://github.com/zed-industries/zed/tree/main/crates/gpui/examples">gpui 的例子</a></h2> <p>对于做前端的我来说,实现 UI 还是很容易的,把它和 WebSocket 服务集成在一起,才是最有挑战的。</p> <p>等我有空了,我再写另一篇文章记录一下我掉进的深坑(主要是不熟悉多线程、异步任务)。</p>
详情
<p>本文将详细介绍基于 iced 实现一个时钟的完整示例。</p> <h1>知识点</h1> <ul> <li>iced 的 subscription</li> <li>窗口图标</li> <li>程序图标</li> <li>自定义字体</li> <li>打包优化</li> <li>在 release 版本中,防止在 Windows 上出现额外的控制台窗口</li> </ul> <h1>预览</h1> <h2>UI</h2> <p><img src="/uploads/Snipaste_2025-07-28_22-51-26.png" alt="" /></p> <h2>优化后的文件大小</h2> <p><img src="/uploads/Snipaste_2025-07-28_22-54-49.png" alt="" /></p> <h1>开发环境</h1> <ul> <li>系统:Windows 11 专业版</li> <li>编辑器:Visual Studio Code</li> <li>cargo:1.85.1 (d73d2caf9 2024-12-31)</li> <li>编译工具链:stable-x86_64-pc-windows-msvc (active, default)</li> </ul> <h1>代码</h1> <h2>Cargo.toml</h2> <pre><code class="language-toml">[package] name = "clock" version = "0.0.1" edition = "2024" build = "build.rs" [dependencies] chrono = "0.4.41" # 时间格式化 iced = { version = "0.13.1", features = ["image", "tokio"] } # image 特性是为了导入窗口图标。tokio 是为了启用 iced::time 定时器 [build-dependencies] winres = "0.1.12" # 为可执行文件添加元信息和图标 [profile.release] strip = true lto = true opt-level = "z"</code></pre> <h2>build.rs</h2> <pre><code class="language-rs">extern crate winres; fn main() { if cfg!(target_os = "windows") { let mut res = winres::WindowsResource::new(); res.set_icon("res/icon.ico"); res.compile().unwrap(); } }</code></pre> <h2>main.rs</h2> <pre><code class="language-rs">// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use std::time::Duration; use chrono::Local; use iced::{ Element, Font, Length, Size, Subscription, widget::{column, container, text}, window::{Icon, Settings, icon}, }; #[derive(Default)] struct ClockApp { current_time: String, } #[derive(Debug, Clone, Copy)] pub enum Message { Tick, } impl ClockApp { pub fn view(&self) -> Element<Message> { container(column![text(&self.current_time).size(26),]) .center(Length::Fill) .into() } pub fn update(&mut self, message: Message) { match message { Message::Tick => { let now = Local::now(); self.current_time = format!("{} {}", now.format("%Y-%m-%d"), now.format("%H:%M:%S")); } } } fn subscription(&self) -> Subscription<Message> { iced::time::every(Duration::from_millis(1000)).map(|_| Message::Tick) } } fn main() -> iced::Result { iced::application("Clock", ClockApp::update, ClockApp::view) .font(include_bytes!("../res/MapleMono-Regular.ttf")) .default_font(Font::with_name("Maple Mono")) .window(Settings { icon: Some(Icon::from( icon::from_file_data(include_bytes!("../res/icon.ico"), None).unwrap(), )), size: Size::new(400.0, 200.0), ..Default::default() }) .subscription(ClockApp::subscription) .run() }</code></pre> <p>注: icon.ico 宽高是 256px * 256px</p> <p><a href="https://font.subf.dev/zh-cn/download/">Maple Mono font</a></p> <h1>参考</h1> <p><a href="https://zhuanlan.zhihu.com/p/16785239921">https://zhuanlan.zhihu.com/p/16785239921</a></p> <p><a href="https://crates.io/crates/winres">https://crates.io/crates/winres</a></p> <p><a href="https://crates.io/crates/chrono">https://crates.io/crates/chrono</a></p> <p><a href="https://docs.rs/iced/latest/iced/index.html#passive-subscriptions">https://docs.rs/iced/latest/iced/index.html#passive-subscriptions</a></p> <p><a href="https://font.subf.dev/zh-cn/download/">https://font.subf.dev/zh-cn/download/</a></p>
详情
<p>最近发现米家 app 里面的小米电视一直显示为离线状态,搜索了一番,终于解决了。</p> <p>解决方案为:在小米电视安装“米家连接服务” app,然后在米家 app 里面添加小米电视。</p> <h1>参考</h1> <p><a href="https://mbd.baidu.com/newspage/data/dtlandingsuper?nid=dt_5001179647010634607&sourceFrom=search_a">小米电视无法添加米家APP?4步解决</a></p>
详情
<p>在 macOS 中,可以通过以下步骤查看当前连接的 WiFi 是 2.4GHz 还是 5GHz 频段:</p> <p>1、连接上目标 WiFi。</p> <p>2、按住键盘上的 Option 键。</p> <p>3、用鼠标点击屏幕顶部的 WiFi 图标,下拉菜单将显示更多信息。</p> <p>4、在下拉菜单中,找到当前连接的WiFi网络名称,向下移动列表,直到看到“频道”这一项。</p> <p>5、在“频道”右侧,会显示连接的通道号,2.4GHz 频段的通道号通常在 1 到 11 之间,而 5GHz 频段的通道号通常在 12 到 165 之间。</p>
详情
<p>最近我的 C 盘由蓝色变为红色(磁盘可用空间只有15GB),心里很郁闷,我最近没有安装新软件,磁盘为什么就会满,经过了一番排查后,发现有一个软件会偷偷截屏并存为图片文件,而且几乎每隔两分钟就截屏一次,非常恶心。</p> <p>为了解决这个问题:</p> <p>1、需要监控某个文件夹,如果文件夹里面新增了文件,则删除。</p> <p>2、服务需要运行在后台。</p> <p>3、服务需要开机自启动。</p> <p>恰好我最近在学 Rust,所以我就使用 Rust 来练练手。</p> <h1>监控文件夹</h1> <pre><code class="language-rust">// 监控的文件夹路径 let folder_to_watch = Path::new("D:\\test"); // 创建一个通道用于接收文件系统事件 let (tx, rx) = channel(); // 创建一个文件监控器 let mut watcher = RecommendedWatcher::new(tx, NotifyConfig::default()).expect("无法创建文件监控器"); // 监听指定文件夹的更改事件 watcher.watch(folder_to_watch, RecursiveMode::NonRecursive).expect("无法监听文件夹"); // 进入事件监听循环 loop { match rx.recv() { Ok(event) => match event { Ok(event) => { if event.kind == EventKind::Create(CreateKind::Any) { for path in event.paths { remove_file(&path); } } } Err(e) => error!("错误: {}", e), }, Err(e) => error!("监控文件错误: {}", e), } } fn remove_file(path: &PathBuf) { if path.is_file() { // 尝试删除文件 sleep(Duration::from_secs(1)); match fs::remove_file(&path) { Ok(_) => info!("成功删除文件: {:?}", path), Err(e) => { error!("删除文件失败: {:?}, 错误: {}", path, e) } } } }</code></pre> <p><code>info!</code> 和 <code>error!</code> 宏来自 <code>log</code> crate。</p> <h1>实现服务</h1> <p>实现服务依赖了 <a href="https://crates.io/crates/windows-service">windows-service</a></p> <p>服务的主体结构是这样的:</p> <pre><code class="language-rust">use log::error; use std::ffi::OsString; use windows_service::service_dispatcher; #[macro_use] extern crate windows_service; define_windows_service!(ffi_service_main, my_service_main); const SERVICE_NAME: &str = "FolderMonitor"; fn my_service_main(arguments: Vec<OsString>) { if let Err(e) = run_service(arguments) { error!("运行服务错误:{:?}", e); } } fn run_service(_args: Vec<OsString>) -> Result<(), windows_service::Error> { // 服务入口 // @TODO 监控文件夹 // @TODO 服务运行时,把服务状态改为运行中 // @TODO 服务停止运行时,退出事件监听循环、停止监控文件夹、把服务状态改为已停止 Ok(()) } fn main() -> Result<(), windows_service::Error> { service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; Ok(()) }</code></pre> <p>完整的<code>run_service</code>代码如下</p> <pre><code class="language-rust">fn run_service(_args: Vec<OsString>) -> Result<(), windows_service::Error> { // 初始化日志系统,将日志写入 LOG_FILE let log_file = OpenOptions::new() .create(true) .append(true) .open(LOG_FILE) .expect("无法打开日志文件"); // 自定义日志格式:包含日期和时间 let config = ConfigBuilder::new().set_time_format_rfc3339().build(); CombinedLogger::init(vec![ TermLogger::new( LevelFilter::Warn, Config::default(), TerminalMode::Mixed, ColorChoice::Auto, ), WriteLogger::new(LevelFilter::Info, config, log_file), ]) .unwrap(); info!("Windows 服务已启动"); // 用于控制主循环的退出 let running = Arc::new(AtomicBool::new(true)); let running_clone = Arc::clone(&running); // 监控的文件夹路径 let folder_to_watch = Path::new(MONITOR_PATH); // 创建一个通道用于接收文件系统事件 let (tx, rx) = channel(); // 创建一个文件监控器 let mut watcher = RecommendedWatcher::new(tx, NotifyConfig::default()).expect("无法创建文件监控器"); // 监听指定文件夹的更改事件 watcher .watch(folder_to_watch, RecursiveMode::NonRecursive) .expect("无法监听文件夹"); // 设定 Windows 服务控制处理器 let status_handle = service_control_handler::register(SERVICE_NAME, move |control| match control { ServiceControl::Stop => { info!("收到服务停止命令"); running_clone.store(false, Ordering::Relaxed); // 更新状态,让主循环退出 return ServiceControlHandlerResult::NoError; } ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, _ => ServiceControlHandlerResult::NotImplemented, }) .unwrap(); status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Running, controls_accepted: ServiceControlAccept::STOP, exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, }) .unwrap(); info!("开始监控文件夹: {}", folder_to_watch.to_string_lossy()); // 进入事件监听循环 while running.load(Ordering::Relaxed) { match rx.recv_timeout(Duration::from_secs(1)) { Ok(event) => match event { Ok(event) => { if event.kind == EventKind::Create(CreateKind::Any) { for path in event.paths { remove_file(&path); } } } Err(e) => error!("错误: {}", e), }, Err(_) => {} } } // 停止监听文件夹变动 if let Err(e) = watcher.unwatch(folder_to_watch) { error!("停止监控文件夹失败: {}", e); } else { info!("已停止监控文件夹: {}", folder_to_watch.to_string_lossy()); } status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, }) .unwrap(); info!("服务已停止"); Ok(()) }</code></pre> <h1>安装服务</h1> <p>以管理员身份运行<code>命令行提示符</code>应用</p> <pre><code class="language-cmd">sc create FolderMonitor binPath= "D:\Program Files\folder-monitor\folder_monitor_service.exe" start= auto # 创建服务并设置为自动启动 sc start FolderMonitor # 启动服务 sc stop FolderMonitor # 停止服务</code></pre> <h1>参考</h1> <p>注:以上研究成果是在<code>ChatGPT</code>帮助下完成。</p> <p><a href="https://crates.io/crates/notify">notify</a></p> <p><a href="https://crates.io/crates/windows-service">windows-service</a></p> <p><a href="https://crates.io/crates/log">log</a></p> <p><a href="https://crates.io/crates/simplelog">simplelog</a></p>
详情
<p><img src="/uploads/rust-module.png" alt="" /></p>
详情
<p>如果你还不了解 Web Bluetooth,请先看我之前写的 <a href="http://pcdeng.com/web-bluetooth.html">《通过 Web Bluetooth 驱动打印机打印》</a></p> <p>Characteristic 可以翻译为特征、特性,本文一律用特征表示 Characteristic,单词太长,不好记,简写为 chatt。</p> <p>这篇文章假设你已经知道怎样连接蓝牙打印机、怎样连接 GATT 服务器,怎样获取 service、怎样获取写特征。</p> <p>开始前,你需要知道服务的 UUID、写特征的 UUID、读特征的 UUID、如果没有读特征的 UUID,要知道通知特征的 UUID。</p> <p>如果不知道,可以通过 LightBlue (iOS app) 连接上蓝牙打印机查看。</p> <p>我手上这台打印机没有读特征。</p> <p>大体步骤如下:</p> <p>1、请求设备 navigator.bluetooth.requestDevice(过滤条件),得到设备 selectedDevice</p> <p>2、连接到 GATT 服务器(打印机)selectedDevice.gatt.connect(),得到 server</p> <p>3、获取主服务 server.getPrimaryService(服务 UUID),得到 service</p> <p>4、获取写特征 service.getCharacteristic(写特征 UUID),得到 writeChatt</p> <p>5、获取通知特征 service.getCharacteristic(通知特征 UUID),得到 notifyChatt</p> <p>读数据的交互流程:</p> <p>1、向打印机写命令,比如 <code>GET MODEL NAME</code>,这不是真实的打印机指令,只是一个例子,具体的指令需要参考自己打印机支持的指令。</p> <p>2、启动通知</p> <p>3、监听 notifyChatt 的 <code>characteristicvaluechanged</code> 事件</p> <p>4、<code>characteristicvaluechanged</code> 事件处理函数处理接收到的数据</p> <p>4、<code>characteristicvaluechanged</code> 事件处理函数最后一步停止通知</p> <p>封装读取函数如下:</p> <pre><code class="language-js">const read = (cmd) => { return new Promise(async (resolve, reject) => { await send(cmd); let timer; const handler = async (evt) => { if (timer) { clearTimeout(timer); } const value = evt.target.value; resolve(value); await notifyChatt.stopNotifications(); }; await notifyChatt.startNotifications(); notifyChatt.addEventListener("characteristicvaluechanged", handler); timer = setTimeout(() => { reject(new Error("Timeout")); }, 5 * 1000); }); };</code></pre>
详情
<p>最近在被一个问题困扰很久,我遇到的场景是这样的。</p> <p>我的应用支持的打印指令包括:ESC/POS、TSPL、ZPL。每当用户通过 USB 连接上打印机后,他需要选择打印机支持的打印指令。如果选错了,可能会乱码或会把打印指令当成内容打印出来。</p> <p>这样就会显得这个应用很不智能,增加了用户心智负担。</p> <p>理想的情况是,连接上打印机后,可以通过一条指令查询出当前打印机支持的指令。而本文介绍的就是这条指令。</p> <p>涉及的关键字有:</p> <ul> <li>USB 控制指令</li> <li>controlTransferIn</li> <li>GET DEVICE ID</li> </ul> <p>话不多说,马上进入正题。</p> <p>前提:这次的示例是基于之前的文章 <a href="/webusb-escpos.html">通过 WebUSB 驱动打印机打印</a>,如果你还没看过,<a href="/webusb-escpos.html">先看</a>再回来看这篇文章。</p> <p>关键函数如下:</p> <pre><code class="language-js">const getInfo = async () => { if (!selectedDevice) { return Promise.reject("未连接打印机"); } const setupParams = { requestType: "class", // 请求类型 request: 0x00, // 是 GET DEVICE ID 的请求代码(通常根据你的设备文档确定)。 value: 0x0000, // 请求值 index: 0x0000, // 接口索引 recipient: "interface", }; const { data, status } = await selectedDevice.controlTransferIn( setupParams, 64 ); if (status != "ok" || !data) { return Promise.reject("读取打印机信息失败"); } const decoder = new TextDecoder(); const deviceId = decoder.decode(data) || ""; console.log(`deviceId ${deviceId}`); // 输出,如:1MFG:Printer;MDL:POS-80;CMD:ESC/POS;CLS:PRINTER; return deviceId; };</code></pre> <p><code>deviceId</code> 是一个字符串,每个字段用<code>;</code>分割。每个字段是<code>KEY:VALUE</code>的形式。不同品牌的打印机,这个 <code>deviceId</code> 可能会返回不同格式的内容,要看具体的打印机返回信息。</p> <p>各个字段的含义:</p> <p>1MFG: 1 可能是版本号或其它的含义。MFG 是 manufacturer 的缩写,代表的是制造商名称。</p> <p>MDL: MDL 是 model 的缩写,代表的是打印机的型号。</p> <p>CMD: CMD 是 command 的缩写,代表的是打印机支持的指令。这是我们最感兴趣的字段。</p> <p>CLS: CLS 是 class 的缩写,代表的是USB 类型。PRINTER 代表的是打印机。</p> <p>通过解析出 CMD 字段,我们就可以拿到打印支持的指令,就不需要用户再选一次打印机支持的指令了。</p> <h1>参考</h1> <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/USBDevice/controlTransferIn">https://developer.mozilla.org/en-US/docs/Web/API/USBDevice/controlTransferIn</a></p> <p>chatgpt, 关键字 <code>webusb 发送 ctl GET DEVICE ID</code></p>
详情
<p>其实这是一个很低级的错误,但它花费了我将近一个小时,重启电脑5次,所以简单记一下。</p> <p>我最近在做一个工具,功能差不多完成了,所以就想着把默认的图标换一下。</p> <h1>准备图标</h1> <p>准备一张 512px * 512px 的 png 图片或 1024px * 1024px 的 png 图片,并命名为 app-icon.png,图片放在项目根目录。</p> <h1>生成图标</h1> <p>在项目根目录执行</p> <pre><code class="language-bash">pnpm tauri icon</code></pre> <p>查看 icon 命令的更多选项</p> <pre><code class="language-bash">$ pnpm tauri icon --help > ble-demo@0.1.0 tauri D:\projects\tauri\ble-demo > tauri "icon" "--help" Generate various icons for all major platforms Usage: pnpm run tauri icon [OPTIONS] [INPUT] Arguments: [INPUT] Path to the source icon (squared PNG or SVG file with transparency) [default: ./app-icon.png] Options: -o, --output <OUTPUT> Output directory. Default: 'icons' directory next to the tauri.conf.json file -v, --verbose... Enables verbose logging -p, --png <PNG> Custom PNG icon sizes to generate. When set, the default icons are not generated --ios-color <IOS_COLOR> The background color of the iOS icon - string as defined in the W3C's CSS Color Module Level 4 <https://www.w3.org/TR/css-color-4/> [default: #fff] -h, --help Print help -V, --version Print version</code></pre> <h1>重新打包</h1> <pre><code class="language-bash">pnpm tauri build</code></pre> <h1>重新安装</h1> <p>发现图标不更新。</p> <h1>解决办法</h1> <p>build 之前需要清除之前的 build 缓存</p> <pre><code class="language-bash">cd src-tauri cargo clean cd .. pnpm tauri build</code></pre>
详情
<p>今天远程帮助一个用户处理应用闪退的问题,昨天已经远程过一次,但是没有解决问题。</p> <p>一、背景。</p> <p>这个应用是一个 Windows 桌面端的应用,基于 Electron 构建的,Electron 应用集成了 crashReporter,也集成了electron-log 用来记录日志。</p> <p>用户的系统是 Windows 7, 64bit。</p> <p>现象是:在桌面上双击打开应用,应用闪一下,然后直接退出。</p> <p>二、第一次远程。</p> <p>第一次远程没有定位到原因。我看了日志,只记录了:发现了一个新版本。看了崩溃日志,发现有一个崩溃 report(C:\Users[登录用户]\AppData\Roaming[应用名称]\Crashpad\reports)。</p> <p>因为发现了崩溃日志,我就以为是由于系统差异而导致的崩溃,但询问客户后,客户反馈说,前几天是可以正常使用的。我就建议客户重启系统,重启系统后远程就断了,之后也无法联系用户了(到下班时间了)。</p> <p>三、第二次远程。</p> <p>自上次远程之后用户一直没有反馈消息,我还以为重启大法生效了。直到下午四点多,用户突然又要求远程协助。心里咯噔一下,我也不知道怎样排查了,难道要分析 dmp 文件(crashReporter 生成的文件)?那就很麻烦了。</p> <p>远程连接上之后,按照昨天的排查思路排查了一遍,依然没有头绪。</p> <p>重启不行,那就重装,得到用户的同意后,就重新装了应用。重装后打开应用,还是闪退。</p> <p>覆盖安装不行,那就彻底删除后再安装。但是,还是闪退。</p> <p>彻底没头绪了,和同事说了一嘴,他回应说:会不会是权限问题?我回答说,用户之前是可以用的,应该不会是权限问题。然后他又说,会不会是被安全软件拦截了?然后我就排查用户的电脑是不是安装了安全软件,看了一下,发现用户的电脑安装了 360 安全卫士。我说拦截也会有提示吧?针对这块没有写日志,要怎样验证?</p> <p>同事说,那简单,在命令行执行这个 .exe 看命令行提示就知道了。</p> <p>在桌面对着应用右键,选择“打开文件所在位置”,然后在地址栏输入 cmd 打开 Windows 终端,在命令行输入 xxx.exe 然后命令提示了: xxxxx,看到提示后知道是权限不够。然后另一个同事说,要不先把 360 安全卫士关了试一试。排查:先关掉 360 安全卫士,再打开软件,能正常打开了,欢天喜地,定位到问题了。</p> <p>解决:360 安全卫士里把这个应用添加到可信任列表。</p> <p>至此,问题圆满解决。</p>
详情