1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
use derive_more::{From, IsVariant};
use serde::{Deserialize, Serialize};
use std::{io, path::PathBuf};
use strum::AsRefStr;
#[cfg(feature = "clap")]
use strum::VariantNames;
mod change;
pub use change::*;
mod cmd;
pub use cmd::*;
#[cfg(feature = "clap")]
mod clap_impl;
mod error;
pub use error::*;
mod filesystem;
pub use filesystem::*;
mod map;
pub use map::Map;
mod metadata;
pub use metadata::*;
mod pty;
pub use pty::*;
mod system;
pub use system::*;
mod utils;
pub(crate) use utils::*;
/// Id for a remote process
pub type ProcessId = u32;
/// Mapping of environment variables
pub type Environment = Map;
/// Type alias for a vec of bytes
///
/// NOTE: This only exists to support properly parsing a Vec<u8> from an entire string
/// with clap rather than trying to parse a string as a singular u8
pub type ByteVec = Vec<u8>;
#[cfg(feature = "clap")]
fn parse_byte_vec(src: &str) -> ByteVec {
src.as_bytes().to_vec()
}
/// Represents a wrapper around a distant message, supporting single and batch requests
#[derive(Clone, Debug, From, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum DistantMsg<T> {
Single(T),
Batch(Vec<T>),
}
impl<T> DistantMsg<T> {
/// Returns true if msg has a single payload
pub fn is_single(&self) -> bool {
matches!(self, Self::Single(_))
}
/// Returns reference to single value if msg is single variant
pub fn as_single(&self) -> Option<&T> {
match self {
Self::Single(x) => Some(x),
_ => None,
}
}
/// Returns mutable reference to single value if msg is single variant
pub fn as_mut_single(&mut self) -> Option<&T> {
match self {
Self::Single(x) => Some(x),
_ => None,
}
}
/// Returns the single value if msg is single variant
pub fn into_single(self) -> Option<T> {
match self {
Self::Single(x) => Some(x),
_ => None,
}
}
/// Returns true if msg has a batch of payloads
pub fn is_batch(&self) -> bool {
matches!(self, Self::Batch(_))
}
/// Returns reference to batch value if msg is batch variant
pub fn as_batch(&self) -> Option<&[T]> {
match self {
Self::Batch(x) => Some(x),
_ => None,
}
}
/// Returns mutable reference to batch value if msg is batch variant
pub fn as_mut_batch(&mut self) -> Option<&mut [T]> {
match self {
Self::Batch(x) => Some(x),
_ => None,
}
}
/// Returns the batch value if msg is batch variant
pub fn into_batch(self) -> Option<Vec<T>> {
match self {
Self::Batch(x) => Some(x),
_ => None,
}
}
/// Convert into a collection of payload data
pub fn into_vec(self) -> Vec<T> {
match self {
Self::Single(x) => vec![x],
Self::Batch(x) => x,
}
}
}
#[cfg(feature = "schemars")]
impl<T: schemars::JsonSchema> DistantMsg<T> {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(DistantMsg<T>)
}
}
/// Represents the payload of a request to be performed on the remote machine
#[derive(Clone, Debug, PartialEq, Eq, IsVariant, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
#[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")]
#[cfg_attr(feature = "clap", clap(rename_all = "kebab-case"))]
pub enum DistantRequestData {
/// Reads a file from the specified path on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["cat"]))]
FileRead {
/// The path to the file on the remote machine
path: PathBuf,
},
/// Reads a file from the specified path on the remote machine
/// and treats the contents as text
FileReadText {
/// The path to the file on the remote machine
path: PathBuf,
},
/// Writes a file, creating it if it does not exist, and overwriting any existing content
/// on the remote machine
FileWrite {
/// The path to the file on the remote machine
path: PathBuf,
/// Data for server-side writing of content
#[cfg_attr(feature = "clap", clap(parse(from_str = parse_byte_vec)))]
data: ByteVec,
},
/// Writes a file using text instead of bytes, creating it if it does not exist,
/// and overwriting any existing content on the remote machine
FileWriteText {
/// The path to the file on the remote machine
path: PathBuf,
/// Data for server-side writing of content
text: String,
},
/// Appends to a file, creating it if it does not exist, on the remote machine
FileAppend {
/// The path to the file on the remote machine
path: PathBuf,
/// Data for server-side writing of content
#[cfg_attr(feature = "clap", clap(parse(from_str = parse_byte_vec)))]
data: ByteVec,
},
/// Appends text to a file, creating it if it does not exist, on the remote machine
FileAppendText {
/// The path to the file on the remote machine
path: PathBuf,
/// Data for server-side writing of content
text: String,
},
/// Reads a directory from the specified path on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["ls"]))]
DirRead {
/// The path to the directory on the remote machine
path: PathBuf,
/// Maximum depth to traverse with 0 indicating there is no maximum
/// depth and 1 indicating the most immediate children within the
/// directory
#[serde(default = "one")]
#[cfg_attr(feature = "clap", clap(long, default_value = "1"))]
depth: usize,
/// Whether or not to return absolute or relative paths
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
absolute: bool,
/// Whether or not to canonicalize the resulting paths, meaning
/// returning the canonical, absolute form of a path with all
/// intermediate components normalized and symbolic links resolved
///
/// Note that the flag absolute must be true to have absolute paths
/// returned, even if canonicalize is flagged as true
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
canonicalize: bool,
/// Whether or not to include the root directory in the retrieved
/// entries
///
/// If included, the root directory will also be a canonicalized,
/// absolute path and will not follow any of the other flags
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
include_root: bool,
},
/// Creates a directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["mkdir"]))]
DirCreate {
/// The path to the directory on the remote machine
path: PathBuf,
/// Whether or not to create all parent directories
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
all: bool,
},
/// Removes a file or directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["rm"]))]
Remove {
/// The path to the file or directory on the remote machine
path: PathBuf,
/// Whether or not to remove all contents within directory if is a directory.
/// Does nothing different for files
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
force: bool,
},
/// Copies a file or directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["cp"]))]
Copy {
/// The path to the file or directory on the remote machine
src: PathBuf,
/// New location on the remote machine for copy of file or directory
dst: PathBuf,
},
/// Moves/renames a file or directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["mv"]))]
Rename {
/// The path to the file or directory on the remote machine
src: PathBuf,
/// New location on the remote machine for the file or directory
dst: PathBuf,
},
/// Watches a path for changes
Watch {
/// The path to the file, directory, or symlink on the remote machine
path: PathBuf,
/// If true, will recursively watch for changes within directories, othewise
/// will only watch for changes immediately within directories
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
recursive: bool,
/// Filter to only report back specified changes
#[serde(default)]
#[cfg_attr(
feature = "clap",
clap(long, possible_values = ChangeKind::VARIANTS)
)]
only: Vec<ChangeKind>,
/// Filter to report back changes except these specified changes
#[serde(default)]
#[cfg_attr(
feature = "clap",
clap(long, possible_values = ChangeKind::VARIANTS)
)]
except: Vec<ChangeKind>,
},
/// Unwatches a path for changes, meaning no additional changes will be reported
Unwatch {
/// The path to the file, directory, or symlink on the remote machine
path: PathBuf,
},
/// Checks whether the given path exists
Exists {
/// The path to the file or directory on the remote machine
path: PathBuf,
},
/// Retrieves filesystem metadata for the specified path on the remote machine
Metadata {
/// The path to the file, directory, or symlink on the remote machine
path: PathBuf,
/// Whether or not to include a canonicalized version of the path, meaning
/// returning the canonical, absolute form of a path with all
/// intermediate components normalized and symbolic links resolved
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
canonicalize: bool,
/// Whether or not to follow symlinks to determine absolute file type (dir/file)
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
resolve_file_type: bool,
},
/// Spawns a new process on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["spawn", "run"]))]
ProcSpawn {
/// The full command to run including arguments
#[cfg_attr(feature = "clap", clap(flatten))]
cmd: Cmd,
/// Environment to provide to the remote process
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long, default_value_t = Environment::default()))]
environment: Environment,
/// Alternative current directory for the remote process
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
current_dir: Option<PathBuf>,
/// Whether or not the process should be persistent, meaning that the process will not be
/// killed when the associated client disconnects
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
persist: bool,
/// If provided, will spawn process in a pty, otherwise spawns directly
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
pty: Option<PtySize>,
},
/// Kills a process running on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["kill"]))]
ProcKill {
/// Id of the actively-running process
id: ProcessId,
},
/// Sends additional data to stdin of running process
ProcStdin {
/// Id of the actively-running process to send stdin data
id: ProcessId,
/// Data to send to a process's stdin pipe
#[serde(with = "serde_bytes")]
#[cfg_attr(feature = "schemars", schemars(with = "Vec<u8>"))]
data: Vec<u8>,
},
/// Resize pty of remote process
ProcResizePty {
/// Id of the actively-running process whose pty to resize
id: ProcessId,
/// The new pty dimensions
size: PtySize,
},
/// Retrieve information about the server and the system it is on
SystemInfo {},
}
#[cfg(feature = "schemars")]
impl DistantRequestData {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(DistantRequestData)
}
}
/// Represents the payload of a successful response
#[derive(Clone, Debug, PartialEq, Eq, AsRefStr, IsVariant, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")]
#[strum(serialize_all = "snake_case")]
pub enum DistantResponseData {
/// General okay with no extra data, returned in cases like
/// creating or removing a directory, copying a file, or renaming
/// a file
Ok,
/// General-purpose failure that occurred from some request
Error(Error),
/// Response containing some arbitrary, binary data
Blob {
/// Binary data associated with the response
#[serde(with = "serde_bytes")]
#[cfg_attr(feature = "schemars", schemars(with = "Vec<u8>"))]
data: Vec<u8>,
},
/// Response containing some arbitrary, text data
Text {
/// Text data associated with the response
data: String,
},
/// Response to reading a directory
DirEntries {
/// Entries contained within the requested directory
entries: Vec<DirEntry>,
/// Errors encountered while scanning for entries
errors: Vec<Error>,
},
/// Response to a filesystem change for some watched file, directory, or symlink
Changed(Change),
/// Response to checking if a path exists
Exists { value: bool },
/// Represents metadata about some filesystem object (file, directory, symlink) on remote machine
Metadata(Metadata),
/// Response to starting a new process
ProcSpawned {
/// Arbitrary id associated with running process
id: ProcessId,
},
/// Actively-transmitted stdout as part of running process
ProcStdout {
/// Arbitrary id associated with running process
id: ProcessId,
/// Data read from a process' stdout pipe
#[serde(with = "serde_bytes")]
#[cfg_attr(feature = "schemars", schemars(with = "Vec<u8>"))]
data: Vec<u8>,
},
/// Actively-transmitted stderr as part of running process
ProcStderr {
/// Arbitrary id associated with running process
id: ProcessId,
/// Data read from a process' stderr pipe
#[serde(with = "serde_bytes")]
#[cfg_attr(feature = "schemars", schemars(with = "Vec<u8>"))]
data: Vec<u8>,
},
/// Response to a process finishing
ProcDone {
/// Arbitrary id associated with running process
id: ProcessId,
/// Whether or not termination was successful
success: bool,
/// Exit code associated with termination, will be missing if terminated by signal
code: Option<i32>,
},
/// Response to retrieving information about the server and the system it is on
SystemInfo(SystemInfo),
}
#[cfg(feature = "schemars")]
impl DistantResponseData {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(DistantResponseData)
}
}
impl From<io::Error> for DistantResponseData {
fn from(x: io::Error) -> Self {
Self::Error(Error::from(x))
}
}
/// Used to provide a default serde value of 1
const fn one() -> usize {
1
}