use std::{
collections::HashMap,
fmt::Debug,
path::{Path, PathBuf},
};
use color_eyre::eyre;
use smol::{
channel::{Receiver, Sender, unbounded},
stream::Stream,
};
use crate::platform::Platform;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Error,
Warn,
#[default]
Info,
Debug,
Verbose,
}
impl LogLevel {
#[must_use]
pub const fn to_android_priority(self) -> char {
match self {
Self::Error => 'E',
Self::Warn => 'W',
Self::Info => 'I',
Self::Debug => 'D',
Self::Verbose => 'V',
}
}
#[must_use]
pub const fn to_apple_level(self) -> &'static str {
match self {
Self::Error | Self::Warn | Self::Info => "default",
Self::Debug | Self::Verbose => "debug",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RunOptions {
env_vars: HashMap<String, String>,
log_level: Option<LogLevel>,
}
impl RunOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert_env_var(&mut self, key: String, value: String) {
self.env_vars.insert(key, value);
}
pub fn env_vars(&self) -> impl Iterator<Item = (&str, &str)> {
self.env_vars.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
pub const fn set_log_level(&mut self, level: LogLevel) {
self.log_level = Some(level);
}
#[must_use]
pub const fn log_level(&self) -> Option<LogLevel> {
self.log_level
}
}
#[derive(Debug)]
pub struct Artifact {
bundle_id: String,
path: PathBuf,
}
impl Artifact {
#[must_use]
pub fn new(bundle_id: impl Into<String>, path: PathBuf) -> Self {
Self {
bundle_id: bundle_id.into(),
path,
}
}
#[must_use]
pub const fn bundle_id(&self) -> &str {
self.bundle_id.as_str()
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
pub trait Device: Send {
type Platform: Platform;
fn launch(&self) -> impl Future<Output = eyre::Result<()>> + Send;
fn run(
&self,
artifact: Artifact,
options: RunOptions,
) -> impl Future<Output = Result<Running, FailToRun>> + Send;
fn platform(&self) -> Self::Platform;
}
pub struct Running {
sender: Sender<DeviceEvent>,
receiver: Receiver<DeviceEvent>,
on_drop: Vec<Box<dyn FnOnce() + Send>>,
}
impl Debug for Running {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Running").finish_non_exhaustive()
}
}
impl Running {
#[allow(clippy::missing_panics_doc)]
pub fn new(on_drop: impl FnOnce() + Send + 'static) -> (Self, Sender<DeviceEvent>) {
let (sender, receiver) = unbounded();
sender.try_send(DeviceEvent::Started).unwrap(); (
Self {
sender: sender.clone(),
receiver,
on_drop: vec![Box::new(on_drop)],
},
sender,
)
}
pub fn retain<T: Send + 'static>(&mut self, value: T) {
self.on_drop.push(Box::new(move || {
drop(value);
}));
}
}
impl Stream for Running {
type Item = DeviceEvent;
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let receiver = unsafe { &mut self.get_unchecked_mut().receiver };
unsafe { std::pin::Pin::new_unchecked(receiver) }.poll_next(cx)
}
}
impl Drop for Running {
fn drop(&mut self) {
let _ = self.sender.try_send(DeviceEvent::Stopped);
for f in self.on_drop.drain(..) {
f();
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum FailToRun {
#[error("Invalid artifact")]
InvalidArtifact,
#[error("Failed to install application on device: {0}")]
Install(eyre::Report),
#[error("Failed to launch device: {0}")]
Launch(eyre::Report),
#[error("Failed to run application on device: {0}")]
Run(eyre::Report),
#[error("Failed to package the artifacts: {0}")]
Package(eyre::Report),
#[error("Failed to build the project: {0}")]
Build(eyre::Report),
#[error("Failed to start hot reload server: {0}")]
HotReload(crate::debug::hot_reload::FailToLaunch),
#[error("Application crashed: {0}")]
Crashed(String),
}
#[derive(Debug)]
pub enum DeviceEvent {
Started,
Stopped,
Stdout {
message: String,
},
Stderr {
message: String,
},
Log {
level: tracing::Level,
message: String,
},
Exited,
Crashed(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceKind {
Simulator,
Physical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceState {
Booted,
Shutdown,
Disconnected,
}