Search code examples
c#wpf.net-corewindows-services.net-core-service-worker

Cannot create a windows service with SC.exe using WPF App after its installation C#


I created a WPF Desktop Application as well as a Worker Service (all .NET 6.0 Preview 3), packed them in a .MSI Setup File using Microsoft Visual Studio Installer Projects extension, which installs the WPF Application on the machine.

While the application installs and functions correctly, I had to somehow implement the service installation which should run after the WPF Application would be installed. I created a function for that, which runs sc.exe as administrator and installs the service using Process.Start(), which looks like this:

private static void InstallService()
{
      const string ServiceName = "SomeService";
      var path = Path.GetFullPath(@".\SomeService.exe");

      var psi = new ProcessStartInfo
      {
         FileName = @"C:\Windows\system32\sc.exe",
         Arguments = $"create { ServiceName } binPath= { path } start= auto",
         Verb = "runas",
         UseShellExecute = true,
      };

      try
      {
         Process.Start(psi);
      }
      catch (Exception ex)
      {
         MessageBox.Show($"Installation has failed: { ex.Message + ex.StackTrace }");
      }
}

The problem with this function is that it executes properly when the application is ran in Visual Studio and when it is ran from the 'bin\Release' folder created by Visual Studio. The service is then installed and can be started. When the program, however, is installed using the .MSI package, the service does not install and no MessageBox is displayed, which shows that no exceptions are thrown.

What I have tried:

  • When the function is executed, a UAC prompt is shown and then the process starts. I tried running the entire application as administrator, but that didn't solve the issue.

  • I also tried copying all the files from the 'bin\Release' directory into the one in which the application is installed and replacing every file with the one from 'bin\Release', so that both directories should be the same, but that also didn't solve the issue.

After the installation function is executed, the service should start with another function for starting it:

private static void RunService()
{
      const string ServiceName = "SomeService";

      var psi = new ProcessStartInfo
      {
           FileName = @"C:\Windows\system32\sc.exe",
           Arguments = $"start { ServiceName }",
           Verb = "runas",
           UseShellExecute = true,
      };

      try
      {
           Process.Start(psi);
      }
      catch (Exception ex)
      {
           MessageBox.Show($"Running was not approved or failed: { ex.Message }");
      }
}

This function, however, functions correctly in both cases, although obviously only when the service is previously installed, which cannot be done in the .MSI installed application. As for the use of Process.Start() instead of ServiceController class, the application should not run as administrator by default, and it is not possible with the ServiceController, so I used Process.Start() with Verb = "runas" which runs the process as administrator only showing the UAC prompt when it is needed (starts the service only when it is not already running).

Is there any way to solve this problem and install a Worker Service in a .MSI installed WPF Application?


Solution

  • As I further proceeded to analyze all the possible factors, I finally noticed what was causing the issue in this case.

    Generally, the paths generated by Visual Studio don't have any spaces, and because of that they can be written as a command argument without double quotes. In my case, the paths which contained the Project files also didn't have any spaces, which caused the commands without double quotes to be executed normally. The installation path, however, did contain spaces, as it's designed to be more user-friendly, which caused this piece of code to not execute as intended:

    // the path parameter in the command will end when there will be a space somewhere in the path
    
    Arguments = $"create { ServiceName } binPath= { path } start= auto"
    

    The path variable only contains the full path, which is not wrapped in double quotes.

    To prevent the issue the use of double quotes is necessary, and including the \ symbol notifies the compiler that these double quotes are a part of the string:

    // the path parameter is wrapped in double quotes and will be fully read
    
    Arguments = $"create { ServiceName } binPath= \"{ path }\" start= auto"
    

    When the path would be fully written in the code, the missing double quotes are easy to notice. When, however, using string interpolation, it may cause problems, as it did in my case.