Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ pub fn run() {
pty::resize_pty,
pty::stop_pty_session,
pty::send_pty_query,
pty::list_pty_sessions,
// 通知 & Hooks
hooks::send_notification,
i18n::set_app_locale,
Expand Down
135 changes: 108 additions & 27 deletions src-tauri/src/pty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use tauri::{Emitter, Manager};

use crate::{
cli_detect::resolve_command_path,
state::{PtyKillerMap, PtyMasterMap, PtySessionMeta, PtySessionMetaMap, PtyWriterMap},
state::{now_ms, PtyKillerMap, PtyMasterMap, PtySessionInfo, PtySessionMeta, PtySessionMetaMap, PtyWriterMap},
util::{expand_path, home_dir, resolve_windows_pty_command},
};

Expand Down Expand Up @@ -85,7 +85,8 @@ impl AnsiStripper {
#[tauri::command]
pub async fn start_pty_session(
app: tauri::AppHandle,
session_id: String,
pty_id: String,
session_id: Option<String>,
workdir: String,
command: String,
args: Vec<String>,
Expand All @@ -97,6 +98,16 @@ pub async fn start_pty_session(
use portable_pty::{native_pty_system, CommandBuilder, PtySize};

let expanded = expand_path(&workdir);
let normalized_session_id = session_id
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let kind = if pty_id.starts_with("runner:") {
"runner"
} else if pty_id.starts_with("terminal:") {
"terminal"
} else {
"ephemeral"
};
let runner_type = env
.as_ref()
.and_then(|pairs| {
Expand All @@ -118,12 +129,11 @@ pub async fn start_pty_session(
.to_string()
});

// 先停掉同 session 的旧 PTY
{
let km = pty_killer_map(&app);
let mut km = km.lock().unwrap();
if let Some(mut old) = km.remove(&session_id) {
let _ = old.kill();
let km = km.lock().unwrap();
if km.contains_key(&pty_id) {
return Ok(());
}
}

Expand Down Expand Up @@ -226,33 +236,39 @@ pub async fn start_pty_session(
{
let wm = pty_writer_map(&app);
let mut wm = wm.lock().unwrap();
wm.insert(session_id.clone(), master_writer);
wm.insert(pty_id.clone(), master_writer);
}
{
let km = pty_killer_map(&app);
let mut km = km.lock().unwrap();
km.insert(session_id.clone(), child);
km.insert(pty_id.clone(), child);
}
{
let mm = pty_master_map(&app);
let mut mm = mm.lock().unwrap();
mm.insert(session_id.clone(), pair.master);
mm.insert(pty_id.clone(), pair.master);
}
{
let meta_map = pty_session_meta_map(&app);
let mut meta_map = meta_map.lock().unwrap();
let now = now_ms();
meta_map.insert(
session_id.clone(),
pty_id.clone(),
PtySessionMeta {
session_id: normalized_session_id.clone(),
kind: kind.to_string(),
runner_type,
workdir: expanded.clone(),
created_at_ms: now,
last_active_at_ms: now,
status: "starting".to_string(),
},
);
}

// 读取线程:转发 PTY 输出并检测状态特征
let app_r = app.clone();
let sid_r = session_id.clone();
let sid_r = pty_id.clone();
let killer_map_r = pty_killer_map(&app);
let session_meta_map_r = pty_session_meta_map(&app);
std::thread::spawn(move || {
Expand All @@ -267,9 +283,14 @@ pub async fn start_pty_session(
Ok(n) => {
// 转发原始数据(base64 编码)
let b64 = base64::engine::general_purpose::STANDARD.encode(&buf[..n]);
let legacy_session_id = sid_r.strip_prefix("runner:").unwrap_or(&sid_r);
let _ = app_r.emit(
"pty-data",
serde_json::json!({ "session_id": sid_r, "data": b64 }),
serde_json::json!({
"pty_id": sid_r,
"session_id": legacy_session_id,
"data": b64,
}),
);

// 检测 CLI 状态特征
Expand All @@ -291,7 +312,21 @@ pub async fn start_pty_session(
} else {
"pty-running"
};
let _ = app_r.emit(event, serde_json::json!({ "session_id": sid_r }));
{
let mut meta_map = session_meta_map_r.lock().unwrap();
if let Some(meta) = meta_map.get_mut(&sid_r) {
meta.status = if new_status == 2 { "waiting" } else { "running" }.to_string();
meta.last_active_at_ms = now_ms();
}
}
let legacy_session_id = sid_r.strip_prefix("runner:").unwrap_or(&sid_r);
let _ = app_r.emit(
event,
serde_json::json!({
"pty_id": sid_r,
"session_id": legacy_session_id,
}),
);
}
}
}
Expand All @@ -309,19 +344,26 @@ pub async fn start_pty_session(
meta_map.remove(&sid_r);
}

let _ = app_r.emit("pty-exit", serde_json::json!({ "session_id": sid_r }));
let legacy_session_id = sid_r.strip_prefix("runner:").unwrap_or(&sid_r);
let _ = app_r.emit(
"pty-exit",
serde_json::json!({
"pty_id": sid_r,
"session_id": legacy_session_id,
}),
);
});

Ok(())
}

/// 向 PTY 写入数据(键盘输入,base64 编码)
#[tauri::command]
pub fn write_pty(app: tauri::AppHandle, session_id: String, data: String) -> Result<(), String> {
pub fn write_pty(app: tauri::AppHandle, pty_id: String, data: String) -> Result<(), String> {
use base64::Engine;
let wm = pty_writer_map(&app);
let mut wm = wm.lock().unwrap();
if let Some(writer) = wm.get_mut(&session_id) {
if let Some(writer) = wm.get_mut(&pty_id) {
let bytes = base64::engine::general_purpose::STANDARD
.decode(&data)
.map_err(|e| format!("base64 decode 失败: {e}"))?;
Expand All @@ -336,12 +378,12 @@ pub fn write_pty(app: tauri::AppHandle, session_id: String, data: String) -> Res
#[tauri::command]
pub fn send_pty_query(
app: tauri::AppHandle,
session_id: String,
pty_id: String,
query: String,
) -> Result<(), String> {
let wm = pty_writer_map(&app);
let mut wm = wm.lock().unwrap();
if let Some(writer) = wm.get_mut(&session_id) {
if let Some(writer) = wm.get_mut(&pty_id) {
let mut data = query.into_bytes();
data.push(if cfg!(windows) { b'\r' } else { b'\n' });
writer
Expand All @@ -352,15 +394,15 @@ pub fn send_pty_query(
.map_err(|e| format!("send_pty_query flush 失败: {e}"))?;
Ok(())
} else {
Err(format!("PTY session '{session_id}' 不存在或尚未就绪"))
Err(format!("PTY session '{pty_id}' 不存在或尚未就绪"))
}
}

/// 调整 PTY 大小(cols/rows 至少为 20/5,防止 SIGWINCH 异常)
#[tauri::command]
pub fn resize_pty(
app: tauri::AppHandle,
session_id: String,
pty_id: String,
cols: u16,
rows: u16,
) -> Result<(), String> {
Expand All @@ -369,7 +411,7 @@ pub fn resize_pty(
let rows = rows.max(5);
let mm = pty_master_map(&app);
let mm = mm.lock().unwrap();
if let Some(master) = mm.get(&session_id) {
if let Some(master) = mm.get(&pty_id) {
master
.resize(PtySize {
rows,
Expand All @@ -379,44 +421,83 @@ pub fn resize_pty(
})
.map_err(|e| format!("resize_pty 失败: {e}"))?;
}
{
let meta_map = pty_session_meta_map(&app);
let mut meta_map = meta_map.lock().unwrap();
if let Some(meta) = meta_map.get_mut(&pty_id) {
meta.last_active_at_ms = now_ms();
}
}
Ok(())
}

/// 停止 PTY 会话
#[tauri::command]
pub fn stop_pty_session(app: tauri::AppHandle, session_id: String) -> Result<(), String> {
pub fn stop_pty_session(app: tauri::AppHandle, pty_id: String) -> Result<(), String> {
let mut had_session = false;
{
let km = pty_killer_map(&app);
let mut km = km.lock().unwrap();
if let Some(mut child) = km.remove(&session_id) {
if let Some(mut child) = km.remove(&pty_id) {
had_session = true;
let _ = child.kill();
}
}
{
let wm = pty_writer_map(&app);
let mut wm = wm.lock().unwrap();
if wm.remove(&session_id).is_some() {
if wm.remove(&pty_id).is_some() {
had_session = true;
}
}
{
let mm = pty_master_map(&app);
let mut mm = mm.lock().unwrap();
if mm.remove(&session_id).is_some() {
if mm.remove(&pty_id).is_some() {
had_session = true;
}
}
{
let meta_map = pty_session_meta_map(&app);
let mut meta_map = meta_map.lock().unwrap();
if meta_map.remove(&session_id).is_some() {
if let Some(meta) = meta_map.get_mut(&pty_id) {
meta.status = "exited".to_string();
meta.last_active_at_ms = now_ms();
}
if meta_map.remove(&pty_id).is_some() {
had_session = true;
}
}
if had_session {
let _ = app.emit("pty-exit", serde_json::json!({ "session_id": session_id }));
let legacy_session_id = pty_id.strip_prefix("runner:").unwrap_or(&pty_id);
let _ = app.emit(
"pty-exit",
serde_json::json!({
"pty_id": pty_id,
"session_id": legacy_session_id,
}),
);
}
Ok(())
}

#[tauri::command]
pub fn list_pty_sessions(app: tauri::AppHandle) -> Result<Vec<PtySessionInfo>, String> {
let meta_map = pty_session_meta_map(&app);
let meta_map = meta_map.lock().unwrap();
let mut entries: Vec<PtySessionInfo> = meta_map
.iter()
.map(|(pty_id, meta)| PtySessionInfo {
pty_id: pty_id.clone(),
session_id: meta.session_id.clone(),
kind: meta.kind.clone(),
runner_type: meta.runner_type.clone(),
workdir: meta.workdir.clone(),
created_at_ms: meta.created_at_ms,
last_active_at_ms: meta.last_active_at_ms,
status: meta.status.clone(),
})
.collect();
entries.sort_by(|a, b| a.pty_id.cmp(&b.pty_id));
Ok(entries)
}
33 changes: 27 additions & 6 deletions src-tauri/src/session_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ fn normalize_workdir(path: &str) -> String {
fn active_session_ids(app: &AppHandle) -> Vec<String> {
let km_arc = app.state::<PtyKillerMap>().inner().clone();
let km = km_arc.lock().unwrap();
km.keys().cloned().collect()
km.keys()
.filter_map(|pty_id| pty_id.strip_prefix("runner:").map(ToString::to_string))
.collect()
}

pub(crate) fn resolve_session_ids(app: &AppHandle, routing: &SessionRoutingHint) -> Vec<String> {
Expand All @@ -119,7 +121,8 @@ pub(crate) fn resolve_session_ids(app: &AppHandle, routing: &SessionRoutingHint)
if !active_ids.iter().any(|sid| sid == session_id) {
return Vec::new();
}
let Some(info) = meta.get(session_id) else {
let runner_pty_id = format!("runner:{session_id}");
let Some(info) = meta.get(&runner_pty_id) else {
return Vec::new();
};
if info.runner_type != routing.source.runner_type() {
Expand All @@ -136,7 +139,8 @@ pub(crate) fn resolve_session_ids(app: &AppHandle, routing: &SessionRoutingHint)
let mut cwd_matches: Vec<String> = active_ids
.into_iter()
.filter(|sid| {
let Some(info) = meta.get(sid) else {
let runner_pty_id = format!("runner:{sid}");
let Some(info) = meta.get(&runner_pty_id) else {
return false;
};
if info.runner_type != routing.source.runner_type() {
Expand Down Expand Up @@ -179,19 +183,35 @@ pub fn emit_session_lifecycle(
match signal {
SessionLifecycleSignal::Running => {
for sid in session_ids {
let _ = app.emit("pty-running", serde_json::json!({ "session_id": sid }));
let _ = app.emit(
"pty-running",
serde_json::json!({
"pty_id": format!("runner:{sid}"),
"session_id": sid,
}),
);
}
}
SessionLifecycleSignal::Waiting => {
for sid in session_ids {
let _ = app.emit("pty-waiting", serde_json::json!({ "session_id": sid }));
let _ = app.emit(
"pty-waiting",
serde_json::json!({
"pty_id": format!("runner:{sid}"),
"session_id": sid,
}),
);
}
}
SessionLifecycleSignal::Error { message } => {
for sid in session_ids {
let _ = app.emit(
"pty-error",
serde_json::json!({ "session_id": sid, "error": message.clone() }),
serde_json::json!({
"pty_id": format!("runner:{sid}"),
"session_id": sid,
"error": message.clone(),
}),
);
}
}
Expand All @@ -214,6 +234,7 @@ pub fn emit_session_lifecycle(
let _ = app.emit(
"pty-notification",
serde_json::json!({
"pty_id": format!("runner:{sid}"),
"session_id": sid,
"title": title.clone(),
"message": message.clone(),
Expand Down
Loading