Checking CD-ROM status with Bash
I got a good deal on a bundle of unopened MSDN CDs from 1999. Score! The first thing I wanted to do was image every CD while they were still pristine. Imaging dozens of CDs by hand would be a hassle, so a Bash script could help me out.
I needed a script to:
- Check if the CD-ROM drive has a disc in it
- Once the disk is ready to read, create an image of the disc using
dd
- After imaging, eject the disc tray
- Wait for a new disc to be inserted and start over
Sounds easy enough, but surprisingly, there’s no easy way to read the CD-ROM tray status in Linux without an external program.
ioctl
I searched around and found that the only way to do this is either by installing a program that will tell you the CD-ROM status or write a bit of code that uses ioctl to talk to the CD-ROM device.
The steps to do this with ioctl are:
- Open (get a file handle to) the CD-ROM device, e.g.
/dev/cdrom
- Send the ioctl command
0x5326
which asks for the status. - Receive the result as an integer 0 to 4
Excerpt from uapi/linux/cdrom.h with relevant information:
#define CDROM_DRIVE_STATUS 0x5326
#define CDS_NO_INFO 0
#define CDS_NO_DISC 1
#define CDS_TRAY_OPEN 2
#define CDS_DRIVE_NOT_READY 3
#define CDS_DISC_OK 4
Choosing a language
I saw examples that do this in C and Python. Wanting to make things more flexible (and add extra fun!), I decided to implement the solution code in multiple languages and use one that is available on the system.
I opted for interpreted languages so that the code could just be executed as needed. Implementing with Python, Perl, and Ruby should be quite enough.
Implementations
Each implementation needed to fulfil these requirements:
- Accept the CD-ROM device as the first program argument
- Open the device in non-blocking mode so it doesn’t hang when the disc isn’t ready
- Write the status number to standard output
Since every implementation is being stored in a Bash script, some code golf was called for.
Python
import os, sys, fcntl
fd = os.open(sys.argv[1], os.O_NONBLOCK) or os.exit(1)
print fcntl.ioctl(fd, 0x5326)
os.close(fd)
Pretty simple and straight forward. Read the device from program arguments using sys.argv[1]
. Open the device, send our command 0x5326
and print
it out.
Perl
sysopen(my $fd, $ARGV[0], 2048) or die("unable to open device");
print ioctl($fd, 0x5326, 1);
close($fd);
Quite similar to the Python implementation. The only notable difference here is the 2048
which is the constant value of O_NONBLOCK
. This could have been imported using use Fcntl qw(O_NONBLOCK);
.
Ruby
File.open(ARGV[0], File::NONBLOCK) { |fd| print fd.ioctl(0x5326) }
Ruby is the clear winner for code golf. One line of clean code. Very nice indeed.
Running from Bash
To keep things easily portable, we can embed the code directly into a Bash script and execute it with the interpreter.
The interpreters ruby
, perl
, and python
all support running code sent to standard input and passing an argument at the same time.
To keep things clean, we’ll create a function for each one using heredocs to pipe the code into the interpreter. $1
is the first argument of the function which we’ll pass as the first argument to the interpreter.
For example, with Python:
cdstat::python() {
python - $1 <<'EOF_PY'
import os, sys, fcntl
fd = os.open(sys.argv[1], os.O_NONBLOCK) or os.exit(1)
print fcntl.ioctl(fd, 0x5326)
os.close(fd)
EOF_PY
}
Selecting the language
We can automatically detect which language to use by looping over a list of interpreter candidates and checking if they exist with command -v
. The first one we hit is the one to use.
cdstat=""
candidates=(ruby perl python)
for candidate in ${candidates[@]}; do
if command -v $candidate 1>/dev/null; then
cdstat="cdstat::$candidate"
break
fi
done
if [ -z "$cdstat" ]; then
echo "could not find any environment to run" >&2
echo "tried: ${candidates[*]}" >&2
exit 1
fi
Now $cdstat
references one of our functions we can execute.
If an argument is passed to the shell script, we’ll use it as our CD-ROM device, otherwise /dev/cdrom
is a sensible default on most Linux distributions.
device=${1:-/dev/cdrom}
$cdstat $device
Wrapping-up
If we put this all together in a script called cdstat.sh
and chmod +x cdstat.sh
, we now have a self-contained executable to read the status of the CD-ROM tray.
#!/bin/bash
set -euo pipefail
cdstat::ruby() {
ruby - $1 <<'EOF_RB'
File.open(ARGV[0], File::NONBLOCK) { |fd| print fd.ioctl(0x5326) }
EOF_RB
}
cdstat::perl() {
perl - $1 <<'EOF_PL'
sysopen(my $fd, $ARGV[0], 2048) or die("unable to open device");
print ioctl($fd, 0x5326, 1);
close($fd);
EOF_PL
}
cdstat::python() {
python - $1 <<'EOF_PY'
import os, sys, fcntl
fd = os.open(sys.argv[1], os.O_NONBLOCK) or os.exit(1)
print fcntl.ioctl(fd, 0x5326)
os.close(fd)
EOF_PY
}
cdstat=""
device=${1:-/dev/cdrom}
candidates=(ruby perl python)
for candidate in ${candidates[@]}; do
if command -v $candidate 1>/dev/null; then
cdstat="cdstat::$candidate"
break
fi
done
if [ -z "$cdstat" ]; then
echo "could not find any environment to run" >&2
echo "tried: ${candidates[*]}" >&2
exit 1
fi
statuses=(none no_disc tray_open not_ready ready)
status=$($cdstat $device)
echo "${statuses[$status]}"
$ ./cdstat.sh
tray_open
The robo CD imager can be found on my GitHub: autorip.sh