SCSI Library in C# – Burn CDs and DVDs, Access Hard Disks, etc.

0
187

Introduction

Have you ever wondered how CD/DVD burning programs like Nero and Roxio work? They can’t just treat CDs as hard disks and write to them; Windows, at least, doesn’t support using file writing functions to write to optical drives. Because of this fact, the only way they can do so is to send commands directly to the drive, bypassing most drivers between the application and the device. Over time, companies have used different methods to achieve this, in most cases by writing their own kernel-mode drivers and communicating with the device directly. It turns out, however, that in the NT family of Windows (including Windows NT, 2000, XP, Vista, 7, etc.), there is a nice generic function that exists precisely for bypassing the OS: DeviceIoControl(), using the IOCTL_SCSI_PASS_THROUGH_DIRECT control code.

Notice the word SCSI? It turns out that CD burning is related to the Small Computer System Interface specifications published by the T10 Committee. In fact, it also turns out that these specifications are useful for communicating with non-multimedia devices (as CD/DVD drives are called) as well, including block devices (e.g., hard disks) and stream devices (e.g., tapes). Although it is true that most computers nowadays use the Advanced Technology Attachment (ATA) standard for peripheral devices, it happens that we can also use SCSI commands to communicate with them. (How exactly this occurs, I still don’t know.)

It took me a long time to figure all this out and to put it to use (read: more than just a year or two), because 99%+ of web searches for topics on “CD burning” either yield results for applications like Nero or Operating System features like the Windows Image Mastering API (IMAPI), and for that 1% that talks about SCSI, I probably ignored them, thinking they were irrelevant. Not being one to give up, though, I kept on going back to the problem, until by some magic, I learned about the SCSI standard. The next step, then, became getting hold of a copy of the standard, which at the time was an easy feat: I just went on the T10 website and downloaded all the drafts I could find. Now if you try to do this, however, you won’t be so lucky: they restricted access to the documents a year or two ago, and it’s pretty darn hard to find other copies on the internet. In fact, I have only found one copy of the old version 3 in my searches, and so I recommend you grab a copy before it’s too late: http://www.13thmonkey.org/documentation/SCSI/mmc3r10g.pdf.

Now, you could go ahead and start reading the 471-page document, but I’m pretty sure you wouldn’t want to, so I did that for you. (Actually, that’s a lie; I just read the parts that I needed to get this working, not the whole 471 pages.) The result? This library. SCSI Command Descriptor Blocks (commands, for short), by their nature, are tightly packed into a handful of bytes (mostly 6 to 12, although on very rare occasions, I have seen 32-byte ones too) and then sent to the drive, so you can imagine how cumbersome it would be for the programmer to have to insert a 5-bit integer into a 6-byte CDB with all those bit shifts and bit masks, especially if you have to worry about marshaling from managed to unmanaged code. That’s why I made this library; instead of writing cryptic code like this:


unsafe
{
 
 byte* cdb = stackalloc[10];
 cdb[0] = 0x5B; 
 cdb[1] = 1;
 cdb[2] = 3; 
 cdb[4] = (byte)((trackNo & 0x0000FF00) > 8); 
 cdb[5] = (byte)((trackNo & 0x000000FF) > 0);
 ExecuteCommand(cdb, 10, buffer, bufferOffset, ...);
}

you can write code like this:

cd.CloseTrackOrSession(new CloseSessionTrackCommand()
{
 Function = TrackSessionCloseFunction.CloseSessionOrStopBGFormat,
 TrackNumber = 2,
 Immediate = true
});

It’s more readable, more maintainable, and more easily debuggable.

What this Library is Not

By now, it probably looks like – or at least I hope it looks like – this library is the magic key to CD burning. Well, that’s both true and false. As unbelievable as this might seem, you can’t just throw bytes onto a disc and insert it in a computer, expecting to magically get files in return. That’s what all those file systems (ISO 9660, Joliet, Universal Disc Format, Rock Ridge, and HFS come to mind) are for. Of course, you have to be able to know both how and what to write to a disc, and so this library’s purpose is to take care of the first of those two steps for you. If you know what to write to a disc, you don’t have to worry about the details of SCSI communication; this library will handle that.

How it Works

There really isn’t anything too tricky about how the library works; the difficulty is in actually implementing the packed data structures while presenting a nice interface to the programmer. Marshaling becomes a bit tricky (especially when converting integers to and from big endian format), and so the classes in this library are designed to provide a simple interface without letting the user worry about the implementation details.

Using the code is easy. Here’s a sample method that burns an ISO image file:


static void TestBurn(string filePath)
{
 
 
 using (var file = new Win32FileStream(@"\\.\CdRom0" , 
 FileAccess.ReadWrite))
 using (var cd = new MultimediaDevice(new Win32Spti(file.SafeFileHandle, true), false))
 {
 cd.Interface.LockVolume();
 

 try
 {
 
 cd.Interface.DismountVolume();
 cd.SynchronizeCache(new SynchronizeCache10Command());
 

 
 cd.SetCDSpeed(new SetCDSpeedCommand(ushort.MaxValue, 
 ushort.MaxValue, RotationControl.ConstantLinearVelocity));

 if (cd.CurrentProfile == MultimediaProfile.CDRW)
 
 {
 Console.WriteLine("Blanking (erasing) CD-RW...");
 cd.Blank(new BlankCommand(
 BlankingType.BlankMinimal, true, 0));
 WaitForDeviceReady(cd);
 }

 
 
 var writeParams = cd.GetWriteParameters(
 new ModeSense10Command(PageControl.CurrentValues));
 writeParams.MultiSession = MultiSession.Multisession;
 writeParams.DataBlockType = DataBlockType.Mode1;
 writeParams.WriteType = WriteType.TrackAtOnce;
 writeParams.SessionFormat = SessionFormat.CdromOrCddaOrOtherDataDisc;
 writeParams.TrackMode = TrackMode.Other;
 cd.SetWriteParameters(new ModeSelect10Command(false, true), writeParams);

 bool closeNeeded; 
 IMultimediaDevice iMMD = cd; 
 
 ushort trackNumber = 
 (ushort)(iMMD.FirstTrackNumber + iMMD.TrackCount - 1);
 
 using (var track = iMMD.CreateTrack(trackNumber, out closeNeeded))
 using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
 
 {
 
 var buffer = new byte[cd.CDSectorSize];
 while (fs.Position < fs.Length)
 
 {
 
 fs.Read(buffer, 0, buffer.Length);
 
 track.Write(buffer, 0, buffer.Length);
 Console.WriteLine("Burn progress: {0:P2}", 
 (double)fs.Position / fs.Length);
 }
 }

 cd.SynchronizeCache(new SynchronizeCache10Command());
 

 if (closeNeeded) 
 
 {
 
 cd.CloseTrackOrSession(new CloseSessionOrTrackCommand(true,
 TrackSessionCloseFunction.CloseSessionOrStopBGFormat, 
 trackNumber));
 Console.WriteLine("Closing...");
 WaitForDeviceReady(cd);
 }
 }
 finally { cd.Interface.UnlockVolume(); } 
 }
}

This method pauses until the drive is ready, printing progress information in fixed time intervals:

static void WaitForDeviceReady(MultimediaDevice cd)
{
 Thread.Sleep(50); 
 
 SenseData sense;
 while ((sense = cd.RequestSense()).SenseKey == SenseKey.NotReady &&
 sense.AdditionalSenseCodeAndQualifier == 
 AdditionalSenseInformation.LogicalUnitNotReady_OperationInProgress)
 {
 
 Console.WriteLine("Progress: {0:P2} done", 
 sense.SenseKeySpecific.ProgressIndication.ProgressIndicationFraction);
 Thread.Sleep(500); 
 }
}

The code should be self-explanatory with the comments, but here are some notes:

  • Whenever a function like CloseTrackOrSession() is called, it requires a ScsiCommand object as its input. The commands should be newly created every time, but if they are cached for performance, then their contents should not be assumed to be preserved.
  • This sample code burns the entire session in one track. As soon as the data is flushed (either because you requested it or because the unit’s write buffer becomes empty), the drive automatically writes the lead-out. This means that you must keep on feeding the drive data until you are done, with no gaps in between, since any gap will cause the lead-out to be written and the session to be closed.

Complete Example: UDF 1.02 CD/DVD Burning Program

If you want to test the basic features of the library, take a look at the ISOBurn program. It can burn both ISO images and individual files and folders to CDs/DVDs. Through weeks of debugging and reading ECMA, ISO, and OSTA standards, I’ve managed to make my program burn with the UDF 1.02 file system. The code for that section isn’t pretty or robust, but I’ll improve it in future releases; it’s just to show the capabilities of this library. Here are a couple of screenshots:

Screenshot

Screenshot

Finally…

Regarding multisession burns: I have tried to make multisession burning work, but please note that it is not ideally implemented; old files are logically erased instead of being kept. Nevertheless, it should work fine without any errors, assuming the last session on the disc is not finalized.

Make sure to check out the MMC-3 documentation mentioned in the introduction as a reference, though you certainly don’t need to concern yourself with the minute details. If you have any questions or comments, please ask! I want to make this a better article, and I can’t do that without your help. 🙂

LEAVE A REPLY