I've created a small app to make backups of disks and to restore a disk from a backup. The logic is pretty simple:
CreateFile on \\.\PhysicalDrive<index>DeviceIoControl with FSCTL_LOCK_VOLUME on source + destination handlesDeviceIoControl with FSCTL_DISMOUNT_VOLUME on source + destination handlesReadFile to read from source and write it to destination using WriteFile until all bytes have been written.DeviceIoControl with FSCTL_UNLOCK_VOLUME on both source and destination handlesCloseHandle on both source and destination handlesI always check that all operations are successful before continuing to the next call. This works really well, but I noticed something that is a bit puzzling:
If, while a copy is taking place, the OS tries to do something with the physical disk (which I know is locked) then the next WriteFile fails.
Examples of things that cause this to fail:
Get-DiskThose things all cause the very next WriteFile call to fail, and I have no idea why. According to the documentation I read FSCTL_LOCK_VOLUME should prevent any other process from acquiring a handle to the disk, so I can't understand why those things could cause issues on a locked volume. Can anyone elaborate and provide a way around it?
Here's my code: it's written in PowerShell but it could easily be converted to C# or something similar.
function Start-RawCopy
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName='FileAndFile')]
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName='FileAndDisk')]
[System.IO.FileInfo]
$SourceFile,
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName='DiskAndFile')]
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName='DiskAndDisk')]
[UInt16]
$SourceDiskNumber,
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='FileAndFile')]
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='DiskAndFile')]
[System.IO.FileInfo]
$DestinationFile,
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='FileAndDisk')]
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='DiskAndDisk')]
[UInt16]
$DestinationDiskNumber
)
Begin
{
$FSCTL_LOCK_VOLUME = 0x00090018
$FSCTL_UNLOCK_VOLUME = 0x0009001c
$FSCTL_DISMOUNT_VOLUME = 0x00090020
$DELETE = 0x00010000
$READ_CONTROL = 0x00020000
$WRITE_DAC = 0x00040000
$WRITE_OWNER = 0x00080000
$SYNCHRONIZE = 0x00100000
$STANDARD_RIGHTS_REQUIRED = 0x000F0000
$STANDARD_RIGHTS_READ = $READ_CONTROL
$STANDARD_RIGHTS_WRITE = $READ_CONTROL
$STANDARD_RIGHTS_EXECUTE = $READ_CONTROL
$FILE_READ_DATA = 0x0001 # file & pipe
$FILE_LIST_DIRECTORY = 0x0001 # directory
$FILE_WRITE_DATA = 0x0002 # file & pipe
$FILE_ADD_FILE = 0x0002 # directory
$FILE_APPEND_DATA = 0x0004 # file
$FILE_ADD_SUBDIRECTORY = 0x0004 # directory
$FILE_CREATE_PIPE_INSTANCE = 0x0004 # named pipe
$FILE_READ_EA = 0x0008 # file & directory
$FILE_WRITE_EA = 0x0010 # file & directory
$FILE_EXECUTE = 0x0020 # file
$FILE_TRAVERSE = 0x0020 # directory
$FILE_DELETE_CHILD = 0x0040 # directory
$FILE_READ_ATTRIBUTES = 0x0080 # all
$FILE_WRITE_ATTRIBUTES = 0x0100 # all
$FILE_ALL_ACCESS = $STANDARD_RIGHTS_REQUIRED -bor $SYNCHRONIZE -bor 0x1FF
$FILE_GENERIC_READ = $STANDARD_RIGHTS_READ -bor $FILE_READ_DATA -bor $FILE_READ_ATTRIBUTES -bor $FILE_READ_EA -bor $SYNCHRONIZE
$FILE_GENERIC_WRITE = $STANDARD_RIGHTS_WRITE -bor $FILE_WRITE_DATA -bor $FILE_WRITE_ATTRIBUTES -bor $FILE_WRITE_EA -bor $FILE_APPEND_DATA -bor $SYNCHRONIZE
$FILE_GENERIC_EXECUTE = $STANDARD_RIGHTS_EXECUTE -bor $FILE_READ_ATTRIBUTES -bor $FILE_EXECUTE -bor $SYNCHRONIZE
$FILE_SHARE_DELETE = 0x00000004
$FILE_SHARE_READ = 0x00000001
$FILE_SHARE_WRITE = 0x00000002
$CREATE_NEW = 1
$CREATE_ALWAYS = 2
$OPEN_EXISTING = 3
$OPEN_ALWAYS = 4
$TRUNCATE_EXISTING = 5
Add-Type -AssemblyName System.Core
}
Process
{
# Validate parameters
if ($PSBoundParameters.ContainsKey('SourceFile'))
{
if (!$SourceFile.Exists)
{
throw "Source file does not exist, cannot continue"
}
$source = $SourceFile.FullName
}
else
{
$source = "\\.\PhysicalDrive$SourceDiskNumber"
}
if ($PSBoundParameters.ContainsKey('DestinationFile'))
{
$destination = $DestinationFile.FullName
$creationDisposition = $CREATE_ALWAYS
}
else
{
$destination = "\\.\PhysicalDrive$DestinationDiskNumber"
$creationDisposition = $OPEN_EXISTING
}
try
{
# Start process
Write-Warning "Starting raw copy - $(Get-Date)"
# Open source object
$handleSource = [NativeMethods]::CreateFile($source, $FILE_GENERIC_READ, $FILE_SHARE_READ -bor $FILE_SHARE_WRITE, 0, $OPEN_EXISTING, 0, [IntPtr]::Zero)
if (!$handleSource)
{
throw "Failed to open source object to read"
}
# Clean destination volume
if ($PSBoundParameters.ContainsKey('DestinationDiskNumber'))
{
Get-Disk -Number $DestinationDiskNumber | Get-Partition | Remove-Partition -Confirm:$false
# Without a sleep this will fail on the 5th WriteFile call all the time. Some caching issue?
Start-Sleep -Seconds 5
}
# Open destination object
$handleDestination = [NativeMethods]::CreateFile($destination, $FILE_ALL_ACCESS, $FILE_SHARE_READ -bor $FILE_SHARE_WRITE -bor $FILE_SHARE_DELETE, 0, $creationDisposition, 0, [IntPtr]::Zero)
if (!$handleDestination)
{
throw "Failed to open destination object to write"
}
$i = 0
# Lock and dismount source volume
if ($PSBoundParameters.ContainsKey('SourceDiskNumber'))
{
if (![NativeMethods]::DeviceIoControl($handleSource, $FSCTL_LOCK_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
{
throw "Failed to lock source volume"
}
if (![NativeMethods]::DeviceIoControl($handleSource, $FSCTL_DISMOUNT_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
{
throw "Failed to dismount source volume"
}
}
# Lock and dismount destination volume
if ($PSBoundParameters.ContainsKey('DestinationDiskNumber'))
{
if (![NativeMethods]::DeviceIoControl($handleDestination, $FSCTL_LOCK_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
{
throw "Failed to lock destination volume"
}
if (![NativeMethods]::DeviceIoControl($handleDestination, $FSCTL_DISMOUNT_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
{
throw "Failed to dismount destination volume"
}
}
# Get source size
if ($PSBoundParameters.ContainsKey('SourceDiskNumber'))
{
$sourceSize = (Get-CimInstance -Namespace ROOT/Microsoft/Windows/Storage -Class MSFT_Disk -Filter "Number = $SourceDiskNumber").Size
}
else
{
$sourceSize = $SourceFile.Length
}
$buffer = [Byte[]]::new(32MB)
$bytesRead = [UInt32] 0
$bytesWritten = [UInt32] 0
[UInt64] $totalBytesRead = 0
# Copy data
do
{
if ($totalBytesRead + $buffer.Length -gt $sourceSize)
{
$buffer = [Byte[]]::new($sourceSize - $totalBytesRead)
if ($Validate)
{
$verifyBuffer = [Byte[]]::new($buffer.Length)
}
}
if (![NativeMethods]::ReadFile($handleSource, $buffer, $buffer.Length, [ref] $bytesRead, [IntPtr]::Zero))
{
throw "Failed to read from source"
}
if (![NativeMethods]::WriteFile($handleDestination, $buffer, $bytesRead, [ref] $bytesWritten, [IntPtr]::Zero))
{
throw "Failed to write to destination"
}
if ($bytesRead -ne $bytesWritten)
{
throw "Read $bytesRead bytes but only wrote $bytesWritten bytes"
}
$totalBytesRead += $bytesRead
} until ($totalBytesRead -eq $sourceSize)
}
finally
{
# Unlock source volume
if ($PSBoundParameters.ContainsKey('SourceDiskNumber'))
{
if ($handleSource -and ![NativeMethods]::DeviceIoControl($handleSource, $FSCTL_UNLOCK_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
{
throw "Failed to unlock source volume"
}
}
# Unlock destination volume
if ($PSBoundParameters.ContainsKey('DestinationDiskNumber'))
{
if ($handleDestination -and ![NativeMethods]::DeviceIoControl($handleDestination, $FSCTL_UNLOCK_VOLUME, $null, 0, $null, 0, [ref] $i, [IntPtr]::Zero))
{
throw "Failed to unlock destination volume"
}
}
# Close source handle
if ($handleSource -and ![NativeMethods]::CloseHandle($handleSource))
{
throw "Failed to close source object"
}
# Close destination handle
if ($handleDestination -and ![NativeMethods]::CloseHandle($handleDestination))
{
throw "Failed to close destination object"
}
Write-Warning "Ended raw copy - $(Get-Date)"
}
}
}
User contributions licensed under CC BY-SA 3.0