Skip to main content

linuxutils_system/
eject.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7    fs::{self, File},
8    io::{self, BufRead},
9    process::{Command, ExitCode},
10};
11
12const DEFAULT_DEVICE: &str = "/dev/cdrom";
13
14// CD-ROM ioctls
15const CDROMEJECT: libc::c_ulong = 0x5309;
16const CDROMCLOSETRAY: libc::c_ulong = 0x5319;
17const CDROM_LOCKDOOR: libc::c_ulong = 0x5329;
18const CDROM_DRIVE_STATUS: libc::c_ulong = 0x5326;
19
20// Drive status results
21const CDS_TRAY_OPEN: i32 = 2;
22
23// Floppy ioctl
24const FDEJECT: libc::c_ulong = 0x025a;
25
26// Tape ioctl
27const MTIOCTOP: libc::c_ulong = 0x4d01;
28const MTOFFL: i16 = 7;
29
30// SCSI
31const SG_IO: libc::c_ulong = 0x2285;
32const SG_DXFER_NONE: i32 = -1;
33
34#[repr(C)]
35struct MtOp {
36    mt_op: i16,
37    mt_count: i32,
38}
39
40#[repr(C)]
41struct SgIoHdr {
42    interface_id: i32,
43    dxfer_direction: i32,
44    cmd_len: u8,
45    mx_sb_len: u8,
46    iovec_count: u16,
47    dxfer_len: u32,
48    dxferp: *mut libc::c_void,
49    cmdp: *const u8,
50    sbp: *mut u8,
51    timeout: u32,
52    flags: u32,
53    pack_id: i32,
54    usr_ptr: *mut libc::c_void,
55    status: u8,
56    masked_status: u8,
57    msg_status: u8,
58    sb_len_wr: u8,
59    host_status: u16,
60    driver_status: u16,
61    resid: i32,
62    duration: u32,
63    info: u32,
64}
65
66#[derive(Parser)]
67#[command(name = "eject", about = "Eject removable media")]
68pub struct Args {
69    /// Display default device name
70    #[arg(short = 'd', long = "default")]
71    default: bool,
72
73    /// Force eject, skip device type check
74    #[arg(short = 'F', long)]
75    force: bool,
76
77    /// Use floppy eject method
78    #[arg(short = 'f', long)]
79    floppy: bool,
80
81    /// Lock or unlock hardware eject button (on|off)
82    #[arg(short = 'i', long = "manualeject")]
83    manualeject: Option<String>,
84
85    /// Don't unmount other partitions on the device
86    #[arg(short = 'M', long = "no-partitions-unmount")]
87    no_partitions_unmount: bool,
88
89    /// Don't unmount the device before ejecting
90    #[arg(short = 'm', long = "no-unmount")]
91    no_unmount: bool,
92
93    /// Show device but take no action
94    #[arg(short = 'n', long)]
95    noop: bool,
96
97    /// Use tape drive offline command
98    #[arg(short = 'q', long)]
99    tape: bool,
100
101    /// Use CD-ROM eject command
102    #[arg(short = 'r', long)]
103    cdrom: bool,
104
105    /// Use SCSI eject commands
106    #[arg(short = 's', long)]
107    scsi: bool,
108
109    /// Toggle tray open/close
110    #[arg(short = 'T', long)]
111    traytoggle: bool,
112
113    /// Close CD-ROM tray
114    #[arg(short = 't', long)]
115    trayclose: bool,
116
117    /// Verbose output
118    #[arg(short = 'v', long)]
119    verbose: bool,
120
121    /// Device or mountpoint to eject
122    device: Option<String>,
123}
124
125fn resolve_device(device: &str) -> String {
126    // If it's a mountpoint, find the device from /proc/mounts
127    if let Ok(file) = File::open("/proc/mounts") {
128        for line in io::BufReader::new(file).lines().map_while(Result::ok) {
129            let fields: Vec<&str> = line.split_whitespace().collect();
130            if fields.len() >= 2 && fields[1] == device {
131                return fields[0].to_string();
132            }
133        }
134    }
135    // Follow symlinks
136    fs::canonicalize(device)
137        .ok()
138        .and_then(|p| p.to_str().map(|s| s.to_string()))
139        .unwrap_or_else(|| device.to_string())
140}
141
142fn find_mounts(device: &str) -> Vec<String> {
143    let mut mounts = Vec::new();
144    let Ok(file) = File::open("/proc/mounts") else {
145        return mounts;
146    };
147    for line in io::BufReader::new(file).lines().map_while(Result::ok) {
148        let fields: Vec<&str> = line.split_whitespace().collect();
149        if fields.len() >= 2 && fields[0] == device {
150            mounts.push(fields[1].to_string());
151        }
152    }
153    mounts
154}
155
156fn unmount(mountpoint: &str, verbose: bool) -> io::Result<()> {
157    if verbose {
158        eprintln!("eject: unmounting {mountpoint}");
159    }
160    let status = Command::new("umount").arg(mountpoint).status()?;
161    if status.success() {
162        Ok(())
163    } else {
164        Err(io::Error::other(format!("umount {mountpoint} failed")))
165    }
166}
167
168fn eject_cdrom(fd: i32) -> io::Result<()> {
169    if unsafe { libc::ioctl(fd, CDROMEJECT) } < 0 {
170        Err(io::Error::last_os_error())
171    } else {
172        Ok(())
173    }
174}
175
176fn eject_scsi(fd: i32) -> io::Result<()> {
177    // First unlock
178    let unlock_cmd: [u8; 6] = [0x1e, 0, 0, 0, 0, 0];
179    let mut sense: [u8; 32] = [0; 32];
180
181    let mut hdr: SgIoHdr = unsafe { std::mem::zeroed() };
182    hdr.interface_id = b'S' as i32;
183    hdr.dxfer_direction = SG_DXFER_NONE;
184    hdr.cmd_len = 6;
185    hdr.mx_sb_len = sense.len() as u8;
186    hdr.cmdp = unlock_cmd.as_ptr();
187    hdr.sbp = sense.as_mut_ptr();
188    hdr.timeout = 10000;
189
190    if unsafe { libc::ioctl(fd, SG_IO, &mut hdr) } < 0 {
191        return Err(io::Error::last_os_error());
192    }
193
194    // Then eject: START_STOP with LoEj=1, Start=0
195    let eject_cmd: [u8; 6] = [0x1b, 0, 0, 0, 0x02, 0];
196    let mut sense2: [u8; 32] = [0; 32];
197
198    let mut hdr2: SgIoHdr = unsafe { std::mem::zeroed() };
199    hdr2.interface_id = b'S' as i32;
200    hdr2.dxfer_direction = SG_DXFER_NONE;
201    hdr2.cmd_len = 6;
202    hdr2.mx_sb_len = sense2.len() as u8;
203    hdr2.cmdp = eject_cmd.as_ptr();
204    hdr2.sbp = sense2.as_mut_ptr();
205    hdr2.timeout = 10000;
206
207    if unsafe { libc::ioctl(fd, SG_IO, &mut hdr2) } < 0 {
208        Err(io::Error::last_os_error())
209    } else {
210        Ok(())
211    }
212}
213
214fn eject_floppy(fd: i32) -> io::Result<()> {
215    if unsafe { libc::ioctl(fd, FDEJECT) } < 0 {
216        Err(io::Error::last_os_error())
217    } else {
218        Ok(())
219    }
220}
221
222fn eject_tape(fd: i32) -> io::Result<()> {
223    let op = MtOp {
224        mt_op: MTOFFL,
225        mt_count: 0,
226    };
227    if unsafe { libc::ioctl(fd, MTIOCTOP, &op) } < 0 {
228        Err(io::Error::last_os_error())
229    } else {
230        Ok(())
231    }
232}
233
234fn close_tray(fd: i32) -> io::Result<()> {
235    if unsafe { libc::ioctl(fd, CDROMCLOSETRAY) } < 0 {
236        Err(io::Error::last_os_error())
237    } else {
238        Ok(())
239    }
240}
241
242fn toggle_tray(fd: i32) -> io::Result<()> {
243    let status = unsafe { libc::ioctl(fd, CDROM_DRIVE_STATUS, 0) };
244    if status < 0 {
245        return Err(io::Error::last_os_error());
246    }
247    if status == CDS_TRAY_OPEN {
248        close_tray(fd)
249    } else {
250        eject_cdrom(fd)
251    }
252}
253
254fn lock_door(fd: i32, lock: bool) -> io::Result<()> {
255    let arg: i32 = if lock { 1 } else { 0 };
256    if unsafe { libc::ioctl(fd, CDROM_LOCKDOOR, arg) } < 0 {
257        Err(io::Error::last_os_error())
258    } else {
259        Ok(())
260    }
261}
262
263pub fn run(args: Args) -> ExitCode {
264    if args.default {
265        println!("{DEFAULT_DEVICE}");
266        return ExitCode::SUCCESS;
267    }
268
269    let device = args.device.as_deref().unwrap_or(DEFAULT_DEVICE);
270    let device = resolve_device(device);
271
272    if args.noop {
273        println!("{device}");
274        return ExitCode::SUCCESS;
275    }
276
277    if args.verbose {
278        eprintln!("eject: device is '{device}'");
279    }
280
281    // Unmount if needed
282    if !args.no_unmount
283        && args.manualeject.is_none()
284        && !args.trayclose
285        && !args.traytoggle
286    {
287        let mounts = find_mounts(&device);
288        for mp in &mounts {
289            if let Err(e) = unmount(mp, args.verbose) {
290                eprintln!("eject: {e}");
291                return ExitCode::FAILURE;
292            }
293        }
294    }
295
296    // Open device
297    let flags = if args.force {
298        libc::O_RDONLY | libc::O_NONBLOCK
299    } else {
300        libc::O_RDONLY | libc::O_NONBLOCK | libc::O_EXCL
301    };
302    let dev_cstr = match std::ffi::CString::new(device.as_str()) {
303        Ok(c) => c,
304        Err(_) => {
305            eprintln!("eject: invalid device path");
306            return ExitCode::FAILURE;
307        }
308    };
309    let fd = unsafe { libc::open(dev_cstr.as_ptr(), flags) };
310    if fd < 0 {
311        eprintln!(
312            "eject: failed to open '{device}': {}",
313            io::Error::last_os_error()
314        );
315        return ExitCode::FAILURE;
316    }
317
318    let result = if let Some(ref val) = args.manualeject {
319        match val.as_str() {
320            "on" | "1" => {
321                if args.verbose {
322                    eprintln!("eject: locking door");
323                }
324                lock_door(fd, true)
325            }
326            "off" | "0" => {
327                if args.verbose {
328                    eprintln!("eject: unlocking door");
329                }
330                lock_door(fd, false)
331            }
332            other => {
333                eprintln!(
334                    "eject: invalid --manualeject value '{other}' (expected on|off)"
335                );
336                unsafe { libc::close(fd) };
337                return ExitCode::FAILURE;
338            }
339        }
340    } else if args.trayclose {
341        if args.verbose {
342            eprintln!("eject: closing tray");
343        }
344        close_tray(fd)
345    } else if args.traytoggle {
346        if args.verbose {
347            eprintln!("eject: toggling tray");
348        }
349        toggle_tray(fd)
350    } else {
351        // Eject: try methods in order, or use specific method
352        let specific = args.cdrom || args.scsi || args.floppy || args.tape;
353
354        let mut ok = false;
355
356        if !ok && (args.cdrom || !specific) {
357            if args.verbose {
358                eprintln!("eject: trying CD-ROM eject");
359            }
360            if eject_cdrom(fd).is_ok() {
361                ok = true;
362            }
363        }
364        if !ok && (args.scsi || !specific) {
365            if args.verbose {
366                eprintln!("eject: trying SCSI eject");
367            }
368            if eject_scsi(fd).is_ok() {
369                ok = true;
370            }
371        }
372        if !ok && (args.floppy || !specific) {
373            if args.verbose {
374                eprintln!("eject: trying floppy eject");
375            }
376            if eject_floppy(fd).is_ok() {
377                ok = true;
378            }
379        }
380        if !ok && (args.tape || !specific) {
381            if args.verbose {
382                eprintln!("eject: trying tape eject");
383            }
384            if eject_tape(fd).is_ok() {
385                ok = true;
386            }
387        }
388
389        if ok {
390            Ok(())
391        } else {
392            Err(io::Error::other("all eject methods failed"))
393        }
394    };
395
396    unsafe { libc::close(fd) };
397
398    match result {
399        Ok(()) => ExitCode::SUCCESS,
400        Err(e) => {
401            eprintln!("eject: {device}: {e}");
402            ExitCode::FAILURE
403        }
404    }
405}