Sorry. Apparently it was all my fault. See my self-written answer below for details.
I'm working on a test for a SUBST
-ed drive that's included as a part of a larger method that tests a string to ensure that it's (at least, possibly) a valid path. After a bit of validation, the "encapsulating" method converts the string to a UNC or absolute path, depending on the path that's passed in, to return elsewhere.
A couple of examples:
For drive U:\
mapped to \\SERVERNAME\Share
, using the \HKCU\Network\
key in the Windows Registry to find network drive mappings on the local computer, U:\PublicFolder\SomeFile.txt
becomes \\SERVERNAME\Share\PublicFolder\SomeFile.txt
Alternately, C:\SomeFolder\SomeFile.txt
is left unchanged because (as determined within the method) it's an absolute path to a local, physical drive.
So far, most of this appears to be working well and as expected, but I'm encountering an issue with regards to drives created by the SUBST
command in Windows 10 (at least - I haven't run any tests under another OS at this time because I don't have another available to me right now).
To be honest, I don't have much experience with the SUBST
command and don't use it very often, so, at first, I was having trouble even getting the drive to show up correctly in Windows. After reading through a discussion on the Microsoft Community page (Windows 10 issue "Subst" command doesn't work), I was finally able to get the drive set up "properly" (don't use an elevated command prompt, BTW), but the code I'm using to test for a SUBST
-ed drive - converted to VB.NET from this answer - was still not resolving the full path correctly.
Here's the converted code I'm using (I intend to do some "tweaking" later once I have everything working, but this is the current state):
<DllImport("kernel32.dll", SetLastError:=True)>
Private Shared Function QueryDosDevice(ByVal lpDeviceName As String, ByVal lpTargetPath As System.Text.StringBuilder, ByVal ucchMax As Integer) As UInteger
End Function
Private Shared Function IsSubstPath(ByVal pathToTest As String, <Out> ByRef realPath As String) As Boolean
Dim PathInformation As System.Text.StringBuilder = New System.Text.StringBuilder(260)
Dim DriveLetter As String = Nothing
Dim WinApiResult As UInteger = 0
realPath = Nothing
Try
' Get the drive letter of the path
DriveLetter = IO.Path.GetPathRoot(pathToTest).Replace("\\", "")
Catch ex As ArgumentException
Return False
End Try
WinApiResult = QueryDosDevice(DriveLetter, PathInformation, 260)
If WinApiResult = 0 Then
Dim LastWinError As Integer = Marshal.GetLastWin32Error()
Return False
End If
' If the drive is SUBST'ed, the result will be in the format of "\??\C:\realPath\".
If PathInformation.ToString().StartsWith("\??\") Then
Dim RealRoot As String = PathInformation.ToString().Remove(0, 4)
RealRoot += If(PathInformation.ToString().EndsWith("\"), "", "\")
realPath = IO.Path.Combine(RealRoot, pathToTest.Replace(IO.Path.GetPathRoot(pathToTest), ""))
Return True
End If
realPath = pathToTest
Return False
End Function
Which I call like this for a drive I created using SUBST H: D:\substtest
:
Dim TestFile As New IO.FileInfo("H:\0984\CPI.TXT")
Dim SubstPath As String = String.Empty
Dim FullPath As String = String.Empty
If IsSubstPath(FullPath, SubstPath) Then
FullPath = SubstPath
End If
My expectation is that the IsSubstPath()
method should return D:\substtest\0984\CPI.TXT
via the realPath
variable. Executing SUBST
(without additional parameters) correctly showed the mapping in the command prompt (H:\: => D:\substtest
). Checking the TestFile
object while debugging shows that it's Exists()
property returns True
, so the file system does, apparently, know that it's there.
At this point, every time I execute the code, the QueryDosDevice()
method call returns a value of 0
, although I get varying results from the Marshal.GetLastWin32Error()
call as I continue to try to get this working.
My first attempt after getting the SUBST
-ed drive "properly" set up on my machine resulted in the Marshal.GetLastWin32Error()
returning error code 1008 - ERROR_NO_TOKEN ("An attempt was made to reference a token that does not exist").
Further reading in the linked MS community thread indicated that running SUBST
twice - once in a normal command prompt, and again in an elevated command prompt - should make the drive available to either a regular logged on user as well as any elevated user action. I re-ran SUBST
in an elevated command prompt and tried again using the same testing code as above. This time, Marshal.GetLastWin32Error()
returned error code 6 - ERROR_INVALID_HANDLE ("The handle is invalid.").
Thinking this particular operation might be dependent on the file/path actually existing on the system (as opposed to the .NET IO.FileInfo
or IO.DirectoryInfo
objects), I manually created the specific subfolder and file to represent what I was testing for in my code (H:\0984\CPI.TXT
) and tried it once more (again, using the same code as above):
Once again, the QueryDosDevice()
failed to correctly parse the real path (returned 0
), but this time the Marshal.GetLastWin32Error()
method returned a value of 0 - ERROR_SUCCESS ("The operation completed successfully."). Thinking that, perhaps there was some "flaw" in the code that might unintentionally be skipping a step or something, I checked the PathInformation
variable - the Text.StringBuilder
object that holds the results of the QueryDosDevice()
- in break mode but, alas it's also empty.
NOTE: I also tried using a directory instead of a file, but H:\0984\
resulted in a Marshal.GetLastWin32Error()
return value of 0
while H:\0984
resulted in a value of 6
. Based on the previous testing, this all makes sense but it nonetheless results in an empty PathInformation
variable (failure).
Reading all around the Interwebz, it seems many people are experiencing a variety of issues with SUBST
-ed drives under Windows 10, so I'm left wondering at this point if that's the reason for these unexpected results. Has anyone else encountered these issues and, if so, have you been able to resolve them in code?
In case it matters (as I suppose it certainly might), here are some additional details:
SUBST
path is a RAID 1 pair of drives formatted with NTFS and has plenty of space (178 GB).SUBST
-ed path to my OS drive (C:\
) for the testing, but this gets the same results as above.If I've left anything out, or if you require further clarification, please let me know in the comments and I'll update the question as needed.
Looks like it all comes down to a single line of code to which I wasn't paying close enough attention (line 14 in the code block above):
DriveLetter = IO.Path.GetPathRoot(PathToTest).Replace("\\", "")
The problem here is that the Path.GetPathRoot()
method returns the drive letter in the format H:\
- there's only one backslash (\
), so the .Replace()
method didn't find anything to replace and was passing an "invalid" parameter value (H:\
instead of H:
). The QueryDosDevice()
method will apparently fail if the trailing backslash is there, so I made a quick code edit:
DriveLetter = IO.Path.GetPathRoot(PathToTest).Replace("\", "") ' <--Replace ANY/ALL backslashes
I again tested with my SUBST H: D:\substtest
drive with an existing file/directory structure as above. This time, the QueryDosDevice()
method returned a value of 19
and correctly parsed the real path as D:\substtest\0984\CPI.TXT
.
Then, I deleted the subfolder/file I had created for testing and tried again. Again, it correctly returned the real path as D:\substtest\0984\CPI.TXT
. So, apparently it all comes down to me overlooking a "typo" introduced during my conversion of the code from C# to VB.NET. My apologies. The full, corrected version of the converted code
<DllImport("kernel32.dll", SetLastError:=True)>
Private Shared Function QueryDosDevice(ByVal lpDeviceName As String, ByVal lpTargetPath As System.Text.StringBuilder, ByVal ucchMax As Integer) As UInteger
End Function
Private Shared Function IsSubstPath(ByVal pathToTest As String, <Out> ByRef realPath As String) As Boolean
Dim PathInformation As System.Text.StringBuilder = New System.Text.StringBuilder(260)
Dim DriveLetter As String = Nothing
Dim WinApiResult As UInteger = 0
realPath = Nothing
Try
' Get the drive letter of the path without the trailing backslash
DriveLetter = IO.Path.GetPathRoot(pathToTest).Replace("\", "")
Catch ex As ArgumentException
Return False
End Try
WinApiResult = QueryDosDevice(DriveLetter, PathInformation, 260)
If WinApiResult = 0 Then
Dim LastWinError As Integer = Marshal.GetLastWin32Error()
Return False
End If
' If the drive is SUBST'ed, the result will be in the format of "\??\C:\realPath\".
If PathInformation.ToString().StartsWith("\??\") Then
Dim RealRoot As String = PathInformation.ToString().Remove(0, 4)
RealRoot += If(PathInformation.ToString().EndsWith("\"), "", "\")
realPath = IO.Path.Combine(RealRoot, pathToTest.Replace(IO.Path.GetPathRoot(pathToTest), ""))
Return True
End If
realPath = pathToTest
Return False
End Function
As I said in the question, I intend to do some "tweaking" of this method, but this does work (now).