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
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

use ::build::toolchain::sdk::Sdk;

use crate::Error;
use crate::model::Device;
use crate::model::Mode;


#[cfg(target_os = "macos")]
pub const DEVICE_MOUNT_POINT_DEF: &str = "/Volumes/PLAYDATE";
#[cfg(all(unix, not(target_os = "macos")))]
pub const DEVICE_MOUNT_POINT_DEF: &str = "/run/media/${USER}/PLAYDATE";
#[cfg(not(unix))]
// FIXME: set the real expected path
pub const DEVICE_MOUNT_POINT_DEF: &str = "/TODO/PLAYDATE";


impl Device {
	pub fn mounted(&self) -> bool {
		self.mode == Mode::Storage &&
		self.volume
		    .as_deref()
		    .filter(|p| p.try_exists().ok().unwrap_or_default())
		    .is_some()
	}
}


pub fn mount_point(mount: &Path) -> Result<PathBuf, Error> {
	use ::build::assets::resolver::EnvResolver;

	let path = mount.to_str()
	                .ok_or_else(|| Error::Error(format!("Mount point is not invalid utf-8: {}", mount.display())))
	                .map(|s| EnvResolver::new().str_only(s))?;

	let path: &Path = Path::new(path.as_ref());

	if !path.try_exists()? {
		Ok(path.to_path_buf())
	} else {
		use std::io::{Error as IoError, ErrorKind};
		Err(IoError::new(
			ErrorKind::AlreadyExists,
			format!("Mount point is already exists: {}", mount.display()),
		).into())
	}
}


/// Mount `device` and return `MountHandle` with the given `mount`.
/// `mount` if __expected__ mount-point.
pub fn mount_device(device: &Device, mount: Option<PathBuf>) -> Result<MountHandle, Error> {
	let mount = if let Some(mount) = mount {
		mount
	} else {
		mount_point(Path::new(DEVICE_MOUNT_POINT_DEF))?
	};

	// mount the device:
	let mount_handle = if let Some(sdk) = Sdk::try_new().map_err(|err| error!("{err}")).ok() {
		mount_with(&sdk, &mount, &device)
	} else {
		mount_without_sdk(&mount, &device)
	}?;

	info!(
	      "{device} successfully mounted to {}",
	      mount_handle.path().display()
	);
	Ok(mount_handle)
}


/// Send command to `device` and return `MountHandle` with the given `mount`.
/// A `mount` is __expected__ mount-point.
pub fn mount_with(sdk: &Sdk, mount: &Path, device: &Device) -> Result<MountHandle, Error> {
	let mount = mount_point(mount)?;
	trace!("serial: {device:?}, mount to: {}", mount.display());

	ensure_device_mountable(device)?;
	let tty = device.tty.as_deref().ok_or(Error::unable_to_find_tty(device))?;
	let mut cmd = Command::new(sdk.pdutil());
	cmd.arg(tty).arg("datadisk");
	debug!("Run: {:?}", cmd);
	cmd.status()?.exit_ok()?;

	Ok(MountHandle::new(mount))
}

pub fn mount_by_device_tty_with(sdk: &Sdk, mount: &Path, device_tty: &Path) -> Result<MountHandle, Error> {
	let mount = mount_point(mount)?;
	trace!("tty: {}, mount to: {}", device_tty.display(), mount.display());

	let mut cmd = Command::new(sdk.pdutil());
	cmd.arg(device_tty).arg("datadisk");
	debug!("Run: {:?}", cmd);
	cmd.status()?.exit_ok()?;

	Ok(MountHandle::new(mount))
}


pub fn mount_without_sdk(mount: &Path, device: &Device) -> Result<MountHandle, Error> {
	ensure_device_mountable(device)?;
	send_storage_mode_to_device_without_sdk(mount, device).map_err(|err| {
		                                                      Error::Error(format!("Unable to send command to {device:?}: {err}."))
	                                                      })?;
	Ok(MountHandle::new(mount.to_path_buf()))
}

pub fn send_storage_mode_to_device_without_sdk(mount: &Path, device: &Device) -> Result<MountHandle, Error> {
	device.write("datadisk").map_err(|err| {
		                         error!("{err}");
		                         Error::Error(format!("Unable to send command to {device}."))
	                         })?;
	Ok(MountHandle::new(mount.to_path_buf()))
}


pub fn ensure_device_mountable(device: &Device) -> Result<(), Error> {
	if !device.mounted() {
		Ok(())
	} else {
		let volume = device.volume
		                   .as_deref()
		                   .map(|p| format!(" at {}", p.display()))
		                   .unwrap_or_default();
		Err(Error::Error(format!("{device} is already mounted{}", volume)))
	}
}


pub struct MountHandle {
	path: PathBuf,
	unmount_on_drop: bool,
}

impl MountHandle {
	pub fn new(path: PathBuf) -> Self {
		MountHandle { path,
		              unmount_on_drop: true }
	}

	pub fn unmount_on_drop(&mut self, value: bool) { self.unmount_on_drop = value; }
	pub fn path(&self) -> &Path { &self.path }

	pub(crate) fn set_mount_point(&mut self, path: PathBuf) { self.path = path; }
}

impl Drop for MountHandle {
	fn drop(&mut self) {
		if self.unmount_on_drop {
			debug!("Unmounting {}", self.path.display());
			let _ = unmount(&self.path).map_err(|err| {
				                           error!("{err}");
				                           info!("Please press 'A' on the Playdate to exit Data Disk mode.");
			                           })
			                           .ok();
		}
	}
}


#[cfg(target_os = "macos")]
pub fn unmount(path: &Path) -> Result<(), Error> {
	Command::new("diskutil").arg("eject")
	                        .arg(path)
	                        .status()?
	                        .exit_ok()?;
	Ok(())
}

#[cfg(target_os = "linux")]
pub fn unmount(path: &Path) -> Result<(), Error> {
	Command::new("eject").arg(path).status()?.exit_ok()?;
	Ok(())
}

#[cfg(target_os = "windows")]
pub fn unmount(path: &Path) -> Result<(), Error> {
	warn!("Unmounting not implemented for windows yet.");
	Command::new("eject").arg(path).status()?.exit_ok()?;
	Ok(())
}