Click here to Skip to main content
15,884,177 members
Articles / Programming Languages / C#

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

Rate me:
Please Sign up or sign in to vote.
4.77/5 (48 votes)
19 Jun 2017Ms-PL6 min read 145.2K   8.1K   146  
Ever wonder how programs like Nero work? They make their own SCSI libraries... like this!
using System;
using System.IO;
using System.Windows.Forms;
using Helper.IO;
using Scsi;
using Scsi.Multimedia;
using System.Threading;
using System.Runtime.InteropServices;
namespace ISOBurn
{
	public partial class FormMain : Form
	{
		private MultimediaDevice currentCD;
		private static readonly TimeSpan UPDATE_PAUSE = TimeSpan.FromMilliseconds(25);
		private static readonly TimeSpan SPEED_UPDATE_PAUSE = TimeSpan.FromMilliseconds(250);
		private ProgressInfo lastProgress;
		private int lastProgressReportTick;

		public FormMain() { this.InitializeComponent(); this.peWriteParams.SelectedObject = new WriteParametersPage(); }

		[DllImport("User32.dll")]
		private static extern bool SetCursorPos(int X, int Y);

		private void RefreshCursor() { SetCursorPos(System.Windows.Forms.Control.MousePosition.X, System.Windows.Forms.Control.MousePosition.Y); }

		[DllImport("User32.dll", SetLastError = true)]
		private static extern bool EnableWindow(IntPtr hWnd, [MarshalAs(UnmanagedType.Bool)] bool bEnable);

		private void btnBurn_Click(object sender, EventArgs e)
		{
			var drive = (DriveInfo)this.cmbDevice.SelectedItem;
			this.currentCD = OpenDrive(drive);
			this.gbxOptions.Enabled = false;
			this.btnMkISOFS.Enabled = false;
			this.cmbDevice.Enabled = false;
			this.btnBrowse.Enabled = false;
			this.btnBurn.Enabled = false;
			this.txtFileName.ReadOnly = true;
			this.UseWaitCursor = true;
			this.bwCDBurner.RunWorkerAsync(new DoWorkInfo(this.currentCD));
			this.btnCancel.Enabled = true;
		}

		private MultimediaDevice OpenDrive(DriveInfo drive) { return new MultimediaDevice(new Win32ScsiPassThroughInterface(new Win32FileStream(@"\\.\" + drive.Name.TrimEnd(Path.DirectorySeparatorChar), FileAccess.ReadWrite, FileShare.ReadWrite /*Must be specified in order to be dismountable*/, FileMode.Open, FileOptions.None, Win32FileOptions.None).SafeFileHandle, false), false); }

		protected override void OnLoad(EventArgs e)
		{
			var drives = System.IO.DriveInfo.GetDrives();
			foreach (var drive in drives)
			{
				if (drive.DriveType == System.IO.DriveType.CDRom)
				{
					this.cmbDevice.Items.Add(drive);
					if (this.cmbDevice.SelectedItem == null)
					{
						this.cmbDevice.SelectedItem = drive;
					}
				}
			}
			base.OnLoad(e);
		}

		protected override void OnShown(EventArgs e)
		{
			base.OnShown(e);
		}

		private class DoWorkInfo { public DoWorkInfo(MultimediaDevice device) { this.Device = device; } public MultimediaDevice Device;	}

		private class ProgressInfo
		{
			public ProgressInfo(string description, long completed, long total, BurnStage stage)
			{
				this.Description = description;
				this.Completed = completed;
				this.Total = total;
				this.Stage = stage;
			}
			public string Description;
			public long Completed; public long Total;
			public BurnStage Stage;
		}

		private enum BurnStage { Locking, Dismounting, Flushing, ReadingDiscInformation, SettingParameters, Blanking, Burning, Closing, Unlocking }

		private void bwCDBurner_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
		{
			//The actual burning occurs here, asynchronously from the UI
			this.lastProgressReportTick = Environment.TickCount;
			var info = (DoWorkInfo)e.Argument;

			using (Stream fileStream = new FileInfo(this.txtFileName.Text).OpenRead())
			{
				this.bwCDBurner.ReportProgress(0, new ProgressInfo("Locking drive...", 0, 1, BurnStage.Locking));

				try { info.Device.Interface.LockVolume(); }
				catch (Exception ex) { throw new IOException("Locking the drive failed. Please close any programs that are using the drive.", ex); }
				try
				{
					this.bwCDBurner.ReportProgress(0, new ProgressInfo("Dismounting volume...", 0, 1, BurnStage.Dismounting));
					try { info.Device.Interface.DismountVolume(); }
					catch { }
					IMultimediaDevice iMMD = info.Device;

					this.bwCDBurner.ReportProgress(0, new ProgressInfo("Flushing any intermediate data in the cache...", 0, 1, BurnStage.Flushing));
					iMMD.Flush();

					this.bwCDBurner.ReportProgress(0, new ProgressInfo("Reading disc information...", 0, 1, BurnStage.ReadingDiscInformation));
					var discInfo = info.Device.ReadDiscInformation();

					var writeParams = (WriteParametersPage)this.peWriteParams.SelectedObject;
					this.bwCDBurner.ReportProgress(0, new ProgressInfo("Setting parameters...", 0, 1, BurnStage.SettingParameters));
					info.Device.SetWriteParameters(new ModeSelect10Command(false, true), writeParams);

					this.bwCDBurner.ReportProgress(0, new ProgressInfo("Seeing if this disc is CD-RW...", 0, 1, BurnStage.ReadingDiscInformation));
					SenseData sense;
					if (info.Device.CurrentProfile == MultimediaProfile.CDRW)
					{
						string message;

						if (discInfo.DiscStatus == DiscStatus.Finalized | discInfo.DiscStatus == DiscStatus.Other) { message = "Detected an erasable disc that cannot be written to." + Environment.NewLine + "You cannot write to this disc unless you erase it." + Environment.NewLine + "Would you like to erase the disc?"; }
						else { message = "It seems like you can write to this disc without erasing it." + Environment.NewLine + "However, erasing is HIGHLY recommended (it may not work correctly otherwise)." + Environment.NewLine + "Would you like to quickly erase it?" + Environment.NewLine + "(Nothing will happen if it is already empty.)"; }

						var result = MessageBox.Show(message, "Erase Disc", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
						if (result == DialogResult.Cancel) { return; }
						if (result == DialogResult.Yes)
						{
							info.Device.Blank(new BlankCommand(BlankingType.BlankMinimal, true, 0));

							int prevTick = Environment.TickCount;

							//Get the progress
							while ((sense = info.Device.RequestSense()).SenseKey == SenseKey.NotReady && sense.AdditionalSenseCode == AdditionalSenseCode.LogicalUnitNotReady && sense.AdditionalSenseCodeQualifier == (AdditionalSenseCodeQualifier)7)
							{
								int tick = Environment.TickCount;
								if (tick - prevTick > UPDATE_PAUSE.TotalMilliseconds)
								{
									this.bwCDBurner.ReportProgress((int)(sense.SenseKeySpecific.ProgressIndication.ProgressIndicationFraction * 100), new ProgressInfo("Erasing CD (cannot be canceled)...", sense.SenseKeySpecific.ProgressIndication.ProgressIndication, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Blanking));
									prevTick = tick;
								}
								else { Thread.Sleep(UPDATE_PAUSE); }
							}

							this.bwCDBurner.ReportProgress(100, new ProgressInfo("CD erased, starting burn...", ProgressIndicationBytes.ProgressIndicationDenominator, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Blanking));
						}
					}

					ushort trackNumber = (ushort)(iMMD.FirstTrackNumber + iMMD.TrackCount - 1); //Last track
					bool mustCloseTrack;

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

					using (var track = iMMD.CreateTrack(trackNumber, out mustCloseTrack))
					{
						int prevTick = Environment.TickCount;
						var buffer = new byte[info.Device.BlockSize];
						while (fileStream.Position < fileStream.Length && !this.bwCDBurner.CancellationPending)
						{
							fileStream.Read(buffer, 0, buffer.Length);
							track.Write(buffer, 0, buffer.Length);

							int tick = Environment.TickCount;
							if (tick - prevTick > UPDATE_PAUSE.TotalMilliseconds)
							{
								this.bwCDBurner.ReportProgress((int)(fileStream.Position * 100 / fileStream.Length), new ProgressInfo("Burning data...", fileStream.Position, fileStream.Length, BurnStage.Burning));
								tick = prevTick;
							}
						}
						this.bwCDBurner.ReportProgress(100, new ProgressInfo("Burn completed.", ProgressIndicationBytes.ProgressIndicationDenominator, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Burning));
					}

					if (mustCloseTrack)
					{
						this.bwCDBurner.ReportProgress(0, new ProgressInfo("Closing...", 0, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Closing));
						info.Device.CloseTrackOrSession(new CloseSessionOrTrackCommand(true, TrackSessionCloseFunction.CloseSessionOrStopBGFormat, trackNumber));

						int prevTick = Environment.TickCount;
						//Get the progress
						while ((sense = info.Device.RequestSense()).SenseKey == SenseKey.NotReady && sense.AdditionalSenseCode == AdditionalSenseCode.LogicalUnitNotReady && sense.AdditionalSenseCodeQualifier == (AdditionalSenseCodeQualifier)7)
						{
							int tick = Environment.TickCount;
							if (tick - prevTick > UPDATE_PAUSE.TotalMilliseconds)
							{
								this.bwCDBurner.ReportProgress((int)(sense.SenseKeySpecific.ProgressIndication.ProgressIndicationFraction * 100), new ProgressInfo("Closing (cannot be canceled)...", sense.SenseKeySpecific.ProgressIndication.ProgressIndication, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Closing));
								prevTick = tick;
							}
							else { Thread.Sleep(UPDATE_PAUSE); }
						}
						this.bwCDBurner.ReportProgress(100, new ProgressInfo("Closed.", ProgressIndicationBytes.ProgressIndicationDenominator, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Closing));
					}
					e.Result = e.Argument;
				}
				finally
				{
					try
					{
						this.bwCDBurner.ReportProgress(0, new ProgressInfo("Dismounting volume...", 1, 1, BurnStage.Dismounting));
						info.Device.Interface.DismountVolume();
					}
					catch { }
					this.bwCDBurner.ReportProgress(0, new ProgressInfo("Unlocking drive...", 1, 1, BurnStage.Unlocking));
					info.Device.Interface.UnlockVolume();
				}
			}
		}

		protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
		{
			e.Cancel |= this.bwCDBurner.IsBusy;
			base.OnClosing(e);
		}

		private void bwCDBurner_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
		{
			var state = (ProgressInfo)e.UserState;

			bool setNewProgress;
			var elapsed = TimeSpan.FromMilliseconds(Environment.TickCount - this.lastProgressReportTick);
			if (this.lastProgress != null && this.lastProgress.Stage == state.Stage && state.Stage == BurnStage.Burning)
			{
				double bytesPerSecond = (state.Completed - this.lastProgress.Completed) / elapsed.TotalSeconds;
				if (elapsed >= SPEED_UPDATE_PAUSE && !double.IsInfinity(bytesPerSecond) && !double.IsNaN(bytesPerSecond))
				{
					this.lblInfo.Text = string.Format("{0} ({1:N0} KB/s)", state.Description, bytesPerSecond / 1024);
					setNewProgress = true;
				}
				else { setNewProgress = false; }
			}
			else
			{
				this.lblInfo.Text = state.Description;
				setNewProgress = true;
			}

			if (setNewProgress)
			{
				this.lastProgress = state;
				this.lastProgressReportTick = Environment.TickCount;
			}

			if (state.Total > int.MaxValue) { this.pbMain.Maximum = int.MaxValue; this.pbMain.Value = (int)((double)state.Completed * int.MaxValue / state.Total); }
			else { this.pbMain.Maximum = (int)state.Total; this.pbMain.Value = (int)state.Completed; }
		}

		private void bwCDBurner_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
		{
			this.UseWaitCursor = false;
			this.currentCD.Dispose();
			this.currentCD = null;
			if (e.Error != null)
			{
				MessageBox.Show(string.Format("{0}", e.Error.Message), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
			}
			else if (!this.btnCancel.Enabled) //meaning this was canceled (e.Canceled and bwCDBurner.CancellationPending don't work for some reason)
			{ this.lblInfo.Text = "Burn canceled. You may need to re-insert the CD to refresh Explorer."; }
			else
			{ this.lblInfo.Text = "Burn successful. You may need to re-insert the CD to refresh Explorer."; }
			this.gbxOptions.Enabled = true;
			this.pbMain.Value = this.pbMain.Minimum;
			this.btnBurn.Enabled = true;
			this.btnBrowse.Enabled = true;
			this.txtFileName.ReadOnly = false;
			this.btnCancel.Enabled = false;
			this.btnMkISOFS.Enabled = true;
			this.cmbDevice.Enabled = true;
		}

		private void btnCancel_Click(object sender, EventArgs e)
		{
			if (this.bwCDBurner.IsBusy)
			{
				if (MessageBox.Show(this, @"Are you sure you want to cancel the operation?" + Environment.NewLine + "Please note that some operations (such as closing a track or blanking a disc)" + Environment.NewLine + "cannot be canceled and will be completed before stopping.", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1) == DialogResult.Yes)
				{
					this.bwCDBurner.CancelAsync();
					this.btnCancel.Enabled = false;
				}
			}
		}

		private void btnBrowse_Click(object sender, EventArgs e)
		{
			if (this.ofdMain.ShowDialog(this) == DialogResult.OK)
			{
				this.txtFileName.Text = this.ofdMain.FileName;
			}
		}

		private void miFileExit_Click(object sender, EventArgs e)
		{
			Application.Exit();
		}

		private void txtFileName_TextChanged(object sender, EventArgs e)
		{
			this.btnBurn.Enabled = !string.IsNullOrEmpty(this.txtFileName.Text);
		}

		private void btnMkISOFS_Click(object sender, EventArgs e)
		{
			using (var form = new FormMakeImage())
			{
				var dr = form.ShowDialog(this);
				if (dr == DialogResult.OK)
				{
					this.txtFileName.Text = form.ImageFile.FullName;
				}
			}
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions