Initial notification channel support

This commit is contained in:
Armin Ronacher 2025-06-16 02:57:08 +02:00
parent 1048e21083
commit a9d75c2f04
3 changed files with 84 additions and 6 deletions

View file

@ -38,6 +38,7 @@ fn list_sessions(control_path: &Path) -> Result<(), anyhow::Error> {
let session_json_path = path.join("session.json");
let stream_out_path = path.join("stream-out");
let stdin_path = path.join("stdin");
let notification_stream_path = path.join("notification-stream");
if session_json_path.exists() {
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,
"started_at": session_info.started_at,
"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 {
// Fallback to old behavior if JSON parsing fails
@ -63,7 +65,8 @@ fn list_sessions(control_path: &Path) -> Result<(), anyhow::Error> {
serde_json::json!({
"status": status,
"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 {
@ -76,7 +79,8 @@ fn list_sessions(control_path: &Path) -> Result<(), anyhow::Error> {
serde_json::json!({
"status": status,
"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
let stream_out_path = session_path.join("stream-out");
let stdin_path = session_path.join("stdin");
let notification_stream_path = session_path.join("notification-stream");
// Create and configure TtySpawn
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);
}
// Always enable notification stream
tty_spawn.notification_path(&notification_stream_path)?;
// Spawn the process
let exit_code = tty_spawn.spawn()?;
std::process::exit(exit_code);

View file

@ -65,6 +65,13 @@ pub struct AsciinemaEvent {
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 {
file: std::fs::File,
start_time: std::time::Instant,
@ -109,3 +116,23 @@ impl StreamWriter {
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(())
}
}

View file

@ -10,7 +10,8 @@ use std::sync::Arc;
use tempfile::NamedTempFile;
use crate::protocol::{
AsciinemaEvent, AsciinemaEventType, AsciinemaHeader, SessionInfo, StreamWriter,
AsciinemaEvent, AsciinemaEventType, AsciinemaHeader, NotificationEvent, NotificationWriter,
SessionInfo, StreamWriter,
};
use crate::utils;
use jiff::Timestamp;
@ -55,6 +56,7 @@ impl TtySpawn {
command,
stdin_file: None,
stream_writer: None,
notification_writer: None,
session_json_path: None,
session_name: None,
}),
@ -127,6 +129,18 @@ impl TtySpawn {
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.
pub fn spawn(&mut self) -> Result<i32, io::Error> {
Ok(spawn(
@ -143,6 +157,7 @@ struct SpawnOptions {
command: Vec<OsString>,
stdin_file: Option<File>,
stream_writer: Option<StreamWriter>,
notification_writer: Option<NotificationWriter>,
session_json_path: Option<PathBuf>,
session_name: Option<String>,
}
@ -226,7 +241,7 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Errno> {
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string());
let cmdline = opts
let cmdline: Vec<String> = opts
.command
.iter()
.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);
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)))?;
// 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
// 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(),
stderr_pty,
true, // flush is always enabled
opts.notification_writer.as_mut(),
)?;
// 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));
}
// 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);
}
@ -333,6 +375,7 @@ fn communication_loop(
in_file: Option<&mut File>,
stderr: Option<OwnedFd>,
flush: bool,
_notification_writer: Option<&mut NotificationWriter>,
) -> Result<i32, Errno> {
let mut buf = [0; 4096];
let mut read_stdin = true;