mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Initial notification channel support
This commit is contained in:
parent
1048e21083
commit
a9d75c2f04
3 changed files with 84 additions and 6 deletions
|
|
@ -38,6 +38,7 @@ fn list_sessions(control_path: &Path) -> Result<(), anyhow::Error> {
|
||||||
let session_json_path = path.join("session.json");
|
let session_json_path = path.join("session.json");
|
||||||
let stream_out_path = path.join("stream-out");
|
let stream_out_path = path.join("stream-out");
|
||||||
let stdin_path = path.join("stdin");
|
let stdin_path = path.join("stdin");
|
||||||
|
let notification_stream_path = path.join("notification-stream");
|
||||||
|
|
||||||
if session_json_path.exists() {
|
if session_json_path.exists() {
|
||||||
let session_data = if let Ok(content) = fs::read_to_string(&session_json_path) {
|
let session_data = if let Ok(content) = fs::read_to_string(&session_json_path) {
|
||||||
|
|
@ -51,7 +52,8 @@ fn list_sessions(control_path: &Path) -> Result<(), anyhow::Error> {
|
||||||
"exit_code": session_info.exit_code,
|
"exit_code": session_info.exit_code,
|
||||||
"started_at": session_info.started_at,
|
"started_at": session_info.started_at,
|
||||||
"stream-out": stream_out_path.canonicalize().unwrap_or(stream_out_path.clone()).to_string_lossy(),
|
"stream-out": stream_out_path.canonicalize().unwrap_or(stream_out_path.clone()).to_string_lossy(),
|
||||||
"stdin": stdin_path.canonicalize().unwrap_or(stdin_path.clone()).to_string_lossy()
|
"stdin": stdin_path.canonicalize().unwrap_or(stdin_path.clone()).to_string_lossy(),
|
||||||
|
"notification-stream": notification_stream_path.canonicalize().unwrap_or(notification_stream_path.clone()).to_string_lossy().to_string()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Fallback to old behavior if JSON parsing fails
|
// Fallback to old behavior if JSON parsing fails
|
||||||
|
|
@ -63,7 +65,8 @@ fn list_sessions(control_path: &Path) -> Result<(), anyhow::Error> {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"status": status,
|
"status": status,
|
||||||
"stream-out": stream_out_path.canonicalize().unwrap_or(stream_out_path.clone()).to_string_lossy(),
|
"stream-out": stream_out_path.canonicalize().unwrap_or(stream_out_path.clone()).to_string_lossy(),
|
||||||
"stdin": stdin_path.canonicalize().unwrap_or(stdin_path.clone()).to_string_lossy()
|
"stdin": stdin_path.canonicalize().unwrap_or(stdin_path.clone()).to_string_lossy(),
|
||||||
|
"notification-stream": notification_stream_path.canonicalize().unwrap_or(notification_stream_path.clone()).to_string_lossy().to_string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -76,7 +79,8 @@ fn list_sessions(control_path: &Path) -> Result<(), anyhow::Error> {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"status": status,
|
"status": status,
|
||||||
"stream-out": stream_out_path.canonicalize().unwrap_or(stream_out_path.clone()).to_string_lossy(),
|
"stream-out": stream_out_path.canonicalize().unwrap_or(stream_out_path.clone()).to_string_lossy(),
|
||||||
"stdin": stdin_path.canonicalize().unwrap_or(stdin_path.clone()).to_string_lossy()
|
"stdin": stdin_path.canonicalize().unwrap_or(stdin_path.clone()).to_string_lossy(),
|
||||||
|
"notification-stream": notification_stream_path.canonicalize().unwrap_or(notification_stream_path.clone()).to_string_lossy().to_string()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -316,6 +320,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
// Set up stream-out and stdin paths
|
// Set up stream-out and stdin paths
|
||||||
let stream_out_path = session_path.join("stream-out");
|
let stream_out_path = session_path.join("stream-out");
|
||||||
let stdin_path = session_path.join("stdin");
|
let stdin_path = session_path.join("stdin");
|
||||||
|
let notification_stream_path = session_path.join("notification-stream");
|
||||||
|
|
||||||
// Create and configure TtySpawn
|
// Create and configure TtySpawn
|
||||||
let mut tty_spawn = TtySpawn::new_cmdline(cmdline.iter().map(|s| s.as_os_str()));
|
let mut tty_spawn = TtySpawn::new_cmdline(cmdline.iter().map(|s| s.as_os_str()));
|
||||||
|
|
@ -328,6 +333,9 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
tty_spawn.session_name(name);
|
tty_spawn.session_name(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always enable notification stream
|
||||||
|
tty_spawn.notification_path(¬ification_stream_path)?;
|
||||||
|
|
||||||
// Spawn the process
|
// Spawn the process
|
||||||
let exit_code = tty_spawn.spawn()?;
|
let exit_code = tty_spawn.spawn()?;
|
||||||
std::process::exit(exit_code);
|
std::process::exit(exit_code);
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,13 @@ pub struct AsciinemaEvent {
|
||||||
pub data: String,
|
pub data: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct NotificationEvent {
|
||||||
|
pub timestamp: Timestamp,
|
||||||
|
pub event: String,
|
||||||
|
pub data: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StreamWriter {
|
pub struct StreamWriter {
|
||||||
file: std::fs::File,
|
file: std::fs::File,
|
||||||
start_time: std::time::Instant,
|
start_time: std::time::Instant,
|
||||||
|
|
@ -109,3 +116,23 @@ impl StreamWriter {
|
||||||
self.start_time.elapsed().as_secs_f64()
|
self.start_time.elapsed().as_secs_f64()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct NotificationWriter {
|
||||||
|
file: std::fs::File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotificationWriter {
|
||||||
|
pub fn new(file: std::fs::File) -> Self {
|
||||||
|
Self { file }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_notification(&mut self, event: NotificationEvent) -> Result<(), std::io::Error> {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let event_json = serde_json::to_string(&event)?;
|
||||||
|
writeln!(self.file, "{}", event_json)?;
|
||||||
|
self.file.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ use std::sync::Arc;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
AsciinemaEvent, AsciinemaEventType, AsciinemaHeader, SessionInfo, StreamWriter,
|
AsciinemaEvent, AsciinemaEventType, AsciinemaHeader, NotificationEvent, NotificationWriter,
|
||||||
|
SessionInfo, StreamWriter,
|
||||||
};
|
};
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use jiff::Timestamp;
|
use jiff::Timestamp;
|
||||||
|
|
@ -55,6 +56,7 @@ impl TtySpawn {
|
||||||
command,
|
command,
|
||||||
stdin_file: None,
|
stdin_file: None,
|
||||||
stream_writer: None,
|
stream_writer: None,
|
||||||
|
notification_writer: None,
|
||||||
session_json_path: None,
|
session_json_path: None,
|
||||||
session_name: None,
|
session_name: None,
|
||||||
}),
|
}),
|
||||||
|
|
@ -127,6 +129,18 @@ impl TtySpawn {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets a path as output file for notifications.
|
||||||
|
pub fn notification_path<P: AsRef<Path>>(&mut self, path: P) -> Result<&mut Self, io::Error> {
|
||||||
|
let file = File::options()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(path)?;
|
||||||
|
|
||||||
|
let notification_writer = NotificationWriter::new(file);
|
||||||
|
self.options_mut().notification_writer = Some(notification_writer);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawns the application in the TTY.
|
/// Spawns the application in the TTY.
|
||||||
pub fn spawn(&mut self) -> Result<i32, io::Error> {
|
pub fn spawn(&mut self) -> Result<i32, io::Error> {
|
||||||
Ok(spawn(
|
Ok(spawn(
|
||||||
|
|
@ -143,6 +157,7 @@ struct SpawnOptions {
|
||||||
command: Vec<OsString>,
|
command: Vec<OsString>,
|
||||||
stdin_file: Option<File>,
|
stdin_file: Option<File>,
|
||||||
stream_writer: Option<StreamWriter>,
|
stream_writer: Option<StreamWriter>,
|
||||||
|
notification_writer: Option<NotificationWriter>,
|
||||||
session_json_path: Option<PathBuf>,
|
session_json_path: Option<PathBuf>,
|
||||||
session_name: Option<String>,
|
session_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +241,7 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Errno> {
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|_| "unknown".to_string());
|
.unwrap_or_else(|_| "unknown".to_string());
|
||||||
|
|
||||||
let cmdline = opts
|
let cmdline: Vec<String> = opts
|
||||||
.command
|
.command
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.to_string_lossy().to_string())
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
|
@ -234,8 +249,22 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Errno> {
|
||||||
|
|
||||||
let session_name = opts.session_name.unwrap_or(executable_name);
|
let session_name = opts.session_name.unwrap_or(executable_name);
|
||||||
|
|
||||||
create_session_info(session_json_path, cmdline, session_name, current_dir)
|
create_session_info(session_json_path, cmdline.clone(), session_name.clone(), current_dir.clone())
|
||||||
.map_err(|e| Errno::from_raw(e.raw_os_error().unwrap_or(libc::EIO)))?;
|
.map_err(|e| Errno::from_raw(e.raw_os_error().unwrap_or(libc::EIO)))?;
|
||||||
|
|
||||||
|
// Send session started notification
|
||||||
|
if let Some(ref mut notification_writer) = opts.notification_writer {
|
||||||
|
let notification = NotificationEvent {
|
||||||
|
timestamp: Timestamp::now(),
|
||||||
|
event: "session_started".to_string(),
|
||||||
|
data: serde_json::json!({
|
||||||
|
"cmdline": cmdline,
|
||||||
|
"name": session_name,
|
||||||
|
"cwd": current_dir
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let _ = notification_writer.write_notification(notification);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// if we can't retrieve the terminal atts we're not directly connected
|
// if we can't retrieve the terminal atts we're not directly connected
|
||||||
// to a pty in which case we won't do any of the terminal related
|
// to a pty in which case we won't do any of the terminal related
|
||||||
|
|
@ -293,6 +322,7 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Errno> {
|
||||||
opts.stdin_file.as_mut(),
|
opts.stdin_file.as_mut(),
|
||||||
stderr_pty,
|
stderr_pty,
|
||||||
true, // flush is always enabled
|
true, // flush is always enabled
|
||||||
|
opts.notification_writer.as_mut(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Update session status to exited with exit code
|
// Update session status to exited with exit code
|
||||||
|
|
@ -300,6 +330,18 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Errno> {
|
||||||
let _ = update_session_status(session_json_path, None, "exited", Some(exit_code));
|
let _ = update_session_status(session_json_path, None, "exited", Some(exit_code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send session exited notification
|
||||||
|
if let Some(ref mut notification_writer) = opts.notification_writer {
|
||||||
|
let notification = NotificationEvent {
|
||||||
|
timestamp: Timestamp::now(),
|
||||||
|
event: "session_exited".to_string(),
|
||||||
|
data: serde_json::json!({
|
||||||
|
"exit_code": exit_code
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let _ = notification_writer.write_notification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(exit_code);
|
return Ok(exit_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,6 +375,7 @@ fn communication_loop(
|
||||||
in_file: Option<&mut File>,
|
in_file: Option<&mut File>,
|
||||||
stderr: Option<OwnedFd>,
|
stderr: Option<OwnedFd>,
|
||||||
flush: bool,
|
flush: bool,
|
||||||
|
_notification_writer: Option<&mut NotificationWriter>,
|
||||||
) -> Result<i32, Errno> {
|
) -> Result<i32, Errno> {
|
||||||
let mut buf = [0; 4096];
|
let mut buf = [0; 4096];
|
||||||
let mut read_stdin = true;
|
let mut read_stdin = true;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue