use std::fmt;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::{AppCommand, SessionId};
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct ProcessIdentity {
pub pid: u32,
pub creation_time: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(try_from = "StoredRecord", into = "StoredRecord")]
pub(crate) struct SessionRecord {
pub id: SessionId,
pub supervisor: ProcessIdentity,
pub pipe_name: String,
pub launch_directory: PathBuf,
pub command: AppCommand,
pub started_at_unix_ms: u64,
pub attached: bool,
pub protocol_version: u32,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct StoredRecord {
id: u32,
supervisor_pid: u32,
supervisor_creation_time: u64,
pipe_name: String,
launch_directory: PathBuf,
command: AppCommand,
started_at_unix_ms: u64,
#[serde(default)]
attached: bool,
#[serde(default = "unversioned_protocol")]
protocol_version: u32,
}
const fn unversioned_protocol() -> u32 {
0
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct MalformedRecord;
impl fmt::Display for MalformedRecord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a session record names a positive session id")
}
}
impl TryFrom<StoredRecord> for SessionRecord {
type Error = MalformedRecord;
fn try_from(stored: StoredRecord) -> Result<Self, Self::Error> {
Ok(Self {
id: SessionId::from_u32(stored.id).ok_or(MalformedRecord)?,
supervisor: ProcessIdentity {
pid: stored.supervisor_pid,
creation_time: stored.supervisor_creation_time,
},
pipe_name: stored.pipe_name,
launch_directory: stored.launch_directory,
command: stored.command,
started_at_unix_ms: stored.started_at_unix_ms,
attached: stored.attached,
protocol_version: stored.protocol_version,
})
}
}
impl From<SessionRecord> for StoredRecord {
fn from(record: SessionRecord) -> Self {
Self {
id: record.id.get(),
supervisor_pid: record.supervisor.pid,
supervisor_creation_time: record.supervisor.creation_time,
pipe_name: record.pipe_name,
launch_directory: record.launch_directory,
command: record.command,
started_at_unix_ms: record.started_at_unix_ms,
attached: record.attached,
protocol_version: record.protocol_version,
}
}
}
impl ProcessIdentity {
#[cfg(test)]
pub(crate) fn for_test(pid: u32) -> Self {
Self {
pid,
creation_time: 0,
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::protocol::PROTOCOL_VERSION;
fn sample() -> SessionRecord {
SessionRecord {
id: SessionId::MIN,
supervisor: ProcessIdentity {
pid: 42,
creation_time: 99,
},
pipe_name: r"\\.\pipe\dure-abc".to_string(),
launch_directory: PathBuf::from(r"C:\work"),
command: AppCommand::for_test(&["copilot.exe"]),
started_at_unix_ms: 1,
attached: false,
protocol_version: PROTOCOL_VERSION,
}
}
#[test]
fn round_trips_json() {
let record = sample();
let json = serde_json::to_string(&record).unwrap();
let parsed: SessionRecord = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, record);
}
#[test]
fn the_stored_shape_is_what_a_later_dure_will_read() {
assert_eq!(
serde_json::to_string(&sample()).unwrap(),
concat!(
r#"{"id":1,"supervisor_pid":42,"supervisor_creation_time":99,"#,
r#""pipe_name":"\\\\.\\pipe\\dure-abc","launch_directory":"C:\\work","#,
r#""command":["copilot.exe"],"started_at_unix_ms":1,"attached":false,"#,
r#""protocol_version":1}"#,
)
);
}
#[test]
fn a_record_written_before_versioning_is_not_assumed_compatible() {
let without_version = concat!(
r#"{"id":1,"supervisor_pid":42,"supervisor_creation_time":99,"#,
r#""pipe_name":"p","launch_directory":"C:\\work","#,
r#""command":["copilot.exe"],"started_at_unix_ms":1,"attached":false}"#,
);
let record = serde_json::from_str::<SessionRecord>(without_version).unwrap();
assert_ne!(record.protocol_version, PROTOCOL_VERSION);
}
#[test]
fn a_record_written_before_the_attached_flag_existed_still_reads() {
let without_flag = concat!(
r#"{"id":1,"supervisor_pid":42,"supervisor_creation_time":99,"#,
r#""pipe_name":"p","launch_directory":"C:\\work","#,
r#""command":["copilot.exe"],"started_at_unix_ms":1}"#,
);
let record = serde_json::from_str::<SessionRecord>(without_flag).unwrap();
assert!(!record.attached);
}
#[test]
fn a_record_naming_an_impossible_session_is_refused() {
let zero_id = concat!(
r#"{"id":0,"supervisor_pid":42,"supervisor_creation_time":99,"#,
r#""pipe_name":"p","launch_directory":"C:\\work","#,
r#""command":["copilot.exe"],"started_at_unix_ms":1}"#,
);
let error = serde_json::from_str::<SessionRecord>(zero_id).unwrap_err();
assert!(
error.to_string().contains("positive session id"),
"the refusal must say what is wrong with the file, got {error}"
);
}
}