2

I've run into a problem with a dotnet core 3.1 application that's designed to run as a windows service. The project originates from the Worker Service template in VS2019. Below is the call stack logged into the system event log. I can't find anything on SO or via web search. If I execute the application directly (double-click) or from a command line it runs fine. The exception is only thrown when executed as a windows service.

Description: The process was terminated due to an unhandled exception.
Exception Info: System.UriFormatException: Invalid URI: The format of the URI could not be determined.
   at System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind)
   at System.Uri..ctor(String uriString)
   at BranchMonitor.TfvcServer..ctor(IOptions`1 settings) in C:\Users\some_user\source\repos\BranchMonitor\BranchManager\TfvcServer.cs:line 53
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitIEnumerable(IEnumerableCallSite enumerableCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at BranchMonitor.Program.Main(String[] args) in C:\Users\some_user\source\repos\BranchMonitor\BranchManager\Program.cs:line 28

Here's the method CreateHostBuilder

public static IHostBuilder CreateHostBuilder(string[] args) =>
     Host.CreateDefaultBuilder(args)
         .ConfigureHostConfiguration(hostBuilder =>
         {
            hostBuilder.SetBasePath(Directory.GetCurrentDirectory());
         })
         .ConfigureAppConfiguration((hostContext, builder) =>
         {
            builder.SetBasePath(Directory.GetCurrentDirectory());
            builder.AddJsonFile("appsettings.json", true, true);
            builder.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true, true);
         })
         .ConfigureServices((hostContext, services) =>
         {
            services.AddHostedService<Worker>();
            services.AddSingleton<ITfvcServer, TfvcServer>();
            services.AddOptions();
            services.Configure<DevOpsSettings>(hostContext.Configuration.GetSection("AzureDevOps"));
         })
         .UseWindowsService();

The TfvcServer class summarized is

public class TfvcServer : ITfvcServer
{
   public TfvcServer(IOptions<DevOpsSettings> settings)
   {
      m_server = settings.Value.Server;
      m_collection = settings.Value.Collection;
      m_project = settings.Value.Project;
      m_definitionFolder = settings.Value.BuildDefinitionFolder;

      m_connection = new VssConnection(new Uri($"{m_server}/{m_collection}"),
                         new VssCredentials()
                         {
                            PromptType = CredentialPromptType.DoNotPrompt
                         });

      m_buildClient = m_connection.GetClient<BuildHttpClient>();
      m_tfvcClient = m_connection.GetClient<TfvcHttpClient>();
   }
}

* EDIT * The source of the problem is the values of the DevOpsSettings are empty. Inside the ctor I create a VssConnection object that is failing since the sever (defined in config) is an empty string. So I need to figure out why the configuration settings are empty. appsettings.Production.json. The "AzureDevOps" section is not in appsettings.json.

{
   "Logging": {
      "LogLevel": {
         "Default": "Information",
         "Microsoft": "Warning",
         "Microsoft.Hosting.Lifetime": "Information"
      }
   },
   "AzureDevOps": {
      "Server": "http://tfsprod:8080/tfs",
      "Collection": "MyCollection",
      "DriveLetters": "KLMNOPSUVWXY",
      "Project": "MyProject",
      "PollingDelay":  60
   }
}
BTSoft
  • 129
  • 10
  • The error is in the initialization code in the TfvcServer constructor. Can you show the initialization code? – kirodge May 11 '20 at 16:55
  • Ditto kirodge (see the line `at BranchMonitor.TfvcServer..ctor(IOptions1 settings) in C:\Users\some_user\source\repos\BranchMonitor\BranchManager\TfvcServer.cs:line 53`) – Connor Low May 11 '20 at 16:56
  • 1
    You're seeing a bad uri error, that means whatever uri that's being initialized with your options isn't valid – JSteward May 11 '20 at 16:57
  • If IOptions.Value is null, there's likely a disconnect between your Configuration setup and the appsettings definition. Can you show your appsettings? – devNull May 11 '20 at 17:35
  • @devNull I don't think Options.Value is null since I'm not getting a NullReferenceException. Rather the properties are simply returning empty strings as if the configuration isn't being loaded. – BTSoft May 11 '20 at 18:01
  • @JonK It sounds like your environment-specific settings (i.e. `Production`) aren't being loaded. Can you verify that the `HostingEnvironment.EnvironmentName` is `Production` in the deployed app? You could also verify that the `ASPNETCORE_ENVIRONMENT` is set as `Production` as well – devNull May 11 '20 at 18:16
  • @devNull It is set to Production. – BTSoft May 11 '20 at 18:33
  • I think I may have the answer. Directory.GetCurrentDirectory() when run under the SCM is C:\Windows\System32 whereas when run within a console is the directory containing the EXE. My theory is when running under svchost the program cannot find the configuration file which explains while all the settings are defaulted. – BTSoft May 11 '20 at 20:32

2 Answers2

6

The problem is the current working directory (CWD) when running under the Service Control Manager (SCM) is C:\Windows\System32 (assuming default OS install). Therefore, when the configuration file is attempted to be loaded it cannot find the configuration file since it doesn't exist. This results in default settings from the POCO settings class, which in my case is string.Empty. It works fine under the debugger or a console since the CWD is the container folder of the executable file.

There are a few approaches to solve this. One being configure the service in SCM to pass a parameter being the path to the executable. To me this isn't very clean since now installation requires configuration. This results in added complexity with no value.

Another option is to use

System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)

instead of the call to Directory.GetCurrentDirectory(). From this SO question

A third option is to use

System.AppDomain.CurrentDomain.BaseDirectory

instead of the call to Directory.GetCurrentDirectory(). From this blog

I'm sure there are other possible solutions. The goal is to provide a couple options that work rather than an exhaustive list of solutions. Thank you to everyone who contributed. I really appreciate your time. Thanks to @JSteward for pointing out my brain fart to get me pointed in the right direction to a resolution. I rely on SO to solve issues and I appreciate those who contribute.

BTSoft
  • 129
  • 10
2

Use AppContext.BaseDirectory on .NET 5 to get the truth

gogosweb
  • 101
  • 1
  • 3