That one has been on my to-do list for quite some time but has always been pushed back lower on the list. Now because it is a bit quieter during the summer, I finally set some time aside to solve that issue.
My problem is that I was trying to start a process using different user credentials from my .Net application. It sounds easy especially that the Start method of the Process component provides all the overloaded methods to specify a user id and a password. That’s what I was thinking until I tried it and got the (in)famous “The directory name is invalid” exception.
The downloadable code
This month’s solution contains both VB and C# projects. The solution was created using Visual Studio 2019 but the code should work as well in older versions of the .Net Framework because there isn’t really nothing very fancy.
An old article on the Process component
The Process component is nothing new. As per the official documentation, this class has been around since version 1.1.
Back in 2003, I even wrote an article on that class that you can still refer at https://www.emoreau.com/Entries/Articles/2003/12/The-Process-component.aspx.
Building the UI
I have created a simple UI to test the start of a process under the current user or as a different user by providing a set of credentials.
Figure 1: The demo application in action
Now the most important code
You will find some code in the downloadable solution not shown here that is not important for the issue like the code behind the browse button and the enabling/disabling of the credentials' textboxes depending on which radio button is selected.
The code that is important is the one behind the Launch process button which reads like this:
Private Sub BtnLaunchProcess_Click(sender As Object, e As EventArgs) Handles btnLaunchProcess.Click Try If String.IsNullOrWhiteSpace(txtPath.Text) Then MessageBox.Show("You must provide a process to start", "Missing value") Return End If Dim strFullPath As String = txtPath.Text If Not IO.File.Exists(strFullPath) Then MessageBox.Show("Problem with the Path - it is invalid", "Incorrect value") Return End If Dim strPathOnly As String = IO.Path.GetDirectoryName(strFullPath) Dim strFileName As String = IO.Path.GetFileName(strFullPath) Dim psi As ProcessStartInfo = New ProcessStartInfo(strFileName) If strPathOnly IsNot Nothing Then psi.WorkingDirectory = strPathOnly psi.UseShellExecute = False If optSpecificCredentials.Checked Then If String.IsNullOrWhiteSpace(txtUserID.Text) Then MessageBox.Show("You must provide a User ID", "Missing value") Return End If Dim strUser As String = txtUserID.Text Dim userParts As String() = strUser.Split("\"c) If userParts.Length = 2 Then psi.Domain = userParts(0) psi.UserName = userParts(1) Else psi.UserName = userParts(0) End If If String.IsNullOrWhiteSpace(txtPassword.Text) Then MessageBox.Show("You must provide a password", "Missing value") Return End If Dim strPassword As SecureString = ConvertToSecureString(txtPassword.Text) psi.Password = strPassword End If Process.Start(psi) Catch ex As Win32Exception MessageBox.Show(ex.Message, "Oups...") End Try End Sub
private void BtnLaunchProcess_Click(object sender, EventArgs e) { try { if (string.IsNullOrWhiteSpace(txtPath.Text)) { MessageBox.Show("You must provide a process to start", "Missing value"); return; } string strFullPath = txtPath.Text; if (!System.IO.File.Exists(strFullPath)) { MessageBox.Show("Problem with the Path - it is invalid", "Incorrect value"); return; } string strPathOnly = System.IO.Path.GetDirectoryName(strFullPath); string strFileName = System.IO.Path.GetFileName(strFullPath); ProcessStartInfo psi = new ProcessStartInfo(strFileName); if (strPathOnly != null) psi.WorkingDirectory = strPathOnly; psi.UseShellExecute = false; if (optSpecificCredentials.Checked) { if (string.IsNullOrWhiteSpace(txtUserID.Text)) { MessageBox.Show("You must provide a User ID", "Missing value"); return; } string strUser = txtUserID.Text; string[] userParts = strUser.Split('\\'); if (userParts.Length == 2) { psi.Domain = userParts[0]; psi.UserName = userParts[1]; } else { psi.UserName = userParts[0]; } if (string.IsNullOrWhiteSpace(txtPassword.Text)) { MessageBox.Show("You must provide a password", "Missing value"); return; } SecureString strPassword = ConvertToSecureString(txtPassword.Text); psi.Password = strPassword; } Process.Start(psi); } catch (Win32Exception ex) { MessageBox.Show(ex.Message, "Oups..."); } }
This code starts by validating that a process is provided and that the path is valid. The path and the filename are isolated in their own variable as we will need this information later.
Then a new instance of the ProcessStartInfo class is created. I could have used a different overload of the Start method to provide credentials if needed but I preferred this mechanism where I set the Domain, UserName and Password properties only when required. Notice that the code is working correctly with local accounts as well as Domain accounts as long as … continue to read!
Finally, the Start method of the Process class is called receiving this ProcessStartInfo instance.
A test process to start
I was looking for a simple application that could return the user running the current process. I thought of building my own when I found that Windows is providing one named Whoami.exe. Because it is normally used as a command line, I wrote a very simple batch file (remember those) that I have included in my solution (test.bat). The code of the batch is:
echo off cls whoami.exe pause
By setting the “Process to start” to this batch file, we will then be able to show the user executing the process.
Figure 2: Figure 2: who is running?
Need a SecureString for the password
When specifying a new set of credentials, the password accepted by the ProcessInfo class requires a SecureString instead of a plain old string. This is for security reason. I wrote about this SecureString type in the past: https://www.emoreau.com/Entries/Articles/2006/08/Strings-Strings-Strings.aspx.
To be able to convert the string from the TextBox to a SecureString, you can use this small method:
Private Function ConvertToSecureString(ByVal pPassword As String) As SecureString If String.IsNullOrWhiteSpace(pPassword) Then Throw New ArgumentNullException(NameOf(pPassword)) End If Dim securePassword = New SecureString() For Each c As Char In pPassword securePassword.AppendChar(c) Next securePassword.MakeReadOnly() Return securePassword End Function
private SecureString ConvertToSecureString(string pPassword) { if (string.IsNullOrWhiteSpace(pPassword)) throw new ArgumentNullException(nameof(pPassword)); var securePassword = new SecureString(); foreach (char c in pPassword) securePassword.AppendChar(c); securePassword.MakeReadOnly(); return securePassword; }
What is the issue?
If you run the demo application and set everything correctly, chances are that you will see a result like the one shown in figure 2.
But when trying to run with different credentials, you might get the error shown in figure 3.
That error was not making sense for me. I was using the browse button to get the process name, and the code is also doing a “File.Exists” so you know that at this point that the directory is indeed valid. But the error is still showing that the directory is invalid. What you need to understand, if you read between the lines, is that this directory is invalid for the specified set of credentials (incorrect credentials are giving an explicit error).
Figure 3: How come directory name is invalid?
If you have this issue, check at the Security tab under the properties of the folder containing the process you are trying to start. Your user (or a group to which he belongs) is surely not listed there. In my case, I was trying to run with a domain admin user, but this user had nothing specified for my folder. As soon as I have added the proper privileges, everything started to work as expected.
Conclusion
“The directory name is invalid” is a good example of an error message that is misleading and not accurate. When you start to put your code into some context, you understand that the directory name is invalid for that context.
Issue solved. What’s next on my list?