14

Disclaimer - this is almost the same question as docker container exits immediately even with Console.ReadLine() in a .net core console application - but I don't think accepted answer on this question is satisfactory.

What I am trying to achieve
I am building a console application (it is a HTTP service using ServiceStack) which is built with .NET core (dnxcore50 - this is a console app, not an ASP.NET application). I am running this application in a docker container on a Linux machine. This I have done, and the HTTP service works.

My problem
Having said that 'my service works' - and it does, there is a problem hosting the service in a docker container. I am using Console.ReadLine() after starting up my HTTP listener, but this code does not block within the docker container and the container will exit immediately after starting. I can start the docker container in 'interactive' mode, and the service will sit there listening until I kill the interactive session and then the container will exit.

Code for Repo
The code below is a complete code listing for creating my test .NET core servicestack console application.

public class Program
{
    public static void Main(string[] args)
    {
        new AppHost().Init().Start("http://*:8088/");
        Console.WriteLine("listening on port 8088");
        Console.ReadLine();

    }
}

public class AppHost : AppSelfHostBase
{
    // Initializes your AppHost Instance, with the Service Name and assembly containing the Services
    public AppHost() : base("My Test Service", typeof(MyTestService).GetAssembly()) { }

    // Configure your AppHost with the necessary configuration and dependencies your App needs
    public override void Configure(Container container)
    {

    }
}

public class MyTestService: Service
{
    public TestResponse Any(TestRequest request)
    {
        string message = string.Format("Hello {0}", request.Name);
        Console.WriteLine(message);
        return new TestResponse {Message = message};
    }

}

[Api("Test method")]
[Route("/test/{Name}", "GET", Summary = "Get Message", Notes = "Gets a message incorporating the passed in name")]
public class TestRequest : IReturn<TestResponse>
{
    [ApiMember(Name = "Name", Description = "Your Name", ParameterType = "path", DataType = "string")]
    public string Name { get; set; }
}

public class TestResponse 
{
    [ApiMember(Name = "Message", Description = "A Message", ParameterType = "path", DataType = "string")]
    public string Message { get; set; }
}

The old way of solving this problem
So having previously hosted using Mono (Mono had severe performance issues - hence the switch to .NET core) - the way to fix this behaviour was to use Mono.Posix listen for a kill signal like this:

using Mono.Unix;
using Mono.Unix.Native;

...

static void Main(string[] args)
    {
        //Start your service here...

        // check if we're running on mono
        if (Type.GetType("Mono.Runtime") != null)
        {
            // on mono, processes will usually run as daemons - this allows you to listen
            // for termination signals (ctrl+c, shutdown, etc) and finalize correctly
            UnixSignal.WaitAny(new[] {
                new UnixSignal(Signum.SIGINT),
                new UnixSignal(Signum.SIGTERM),
                new UnixSignal(Signum.SIGQUIT),
                new UnixSignal(Signum.SIGHUP)
            });
        }
        else
        {
            Console.ReadLine();
        }
    }

Now - I understand that this won't work for .NET Core (obviously because Mono.Posix is for Mono!)

The solution outlined in the related article (top of this post) is no use to me - in a production environment, I can't expect to keep a docker container alive by ensuring it has an interactive session available which will keep the Console.ReadLine working because there is a STD-IN stream there...

Is there another way to keep my docker container alive (using the -d (detached) option when invoking docker run) when hosting a .NET Core application?

Code refactor as part of Mythz suggestion

 public static void Main(string[] args)
    {
        Run(new AppHost().Init(), "http://*:8088/");
    }

    public static void Run(ServiceStackHost host, params string[] uris)
    {
        AppSelfHostBase appSelfHostBase = (AppSelfHostBase)host;

        using (IWebHost webHost = appSelfHostBase.ConfigureHost(new WebHostBuilder(), uris).Build())
        {
            ManualResetEventSlim done = new ManualResetEventSlim(false);
            using (CancellationTokenSource cts = new CancellationTokenSource())
            {
                Action shutdown = () =>
                {
                    if (!cts.IsCancellationRequested)
                    {
                        Console.WriteLine("Application is shutting down...");
                        cts.Cancel();
                    }

                    done.Wait();
                };

                Console.CancelKeyPress += (sender, eventArgs) =>
                {
                    shutdown();
                    // Don't terminate the process immediately, wait for the Main thread to exit gracefully.
                    eventArgs.Cancel = true;
                };

                Console.WriteLine("Application started. Press Ctrl+C to shut down.");
                webHost.Run(cts.Token);
                done.Set();
            }
        }
    }

Final Solution!

For Posterity - the solution I have gone with is the code which can be found here (Thanks to Myths for clarification): https://github.com/NetCoreApps/Hello/blob/master/src/SelfHost/Program.cs

Repo of the pertinent code:

public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseStartup<Startup>()
            .UseUrls("http://*:8088/")
            .Build();

        host.Run();
    }
}

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // app.UseStaticFiles();

        app.UseServiceStack(new AppHost());

        app.Run(context =>
        {
            context.Response.Redirect("/metadata");
            return Task.FromResult(0);
        });
    }

In NuGet, I have Microsoft.NETCore.App, ServiceStack.Core and ServiceStack.Kestrel installed.

Community
  • 1
  • 1
Jay
  • 9,561
  • 7
  • 51
  • 72

1 Answers1

11

If you're going to host .NET Core apps in Docker I'd recommend just following the normal .NET Core Hosting API where it calls IWebHost.Run() to block the main thread and keep the Console Application alive.

AppHostSelfBase is just a wrapper around .NET Core's hosting API but calls the non-blocking IWebHost.Start() instead. To get the behavior of IWebHost.Run() you should be able to reuse the same approach of ManualResetEventSlim and Console.CancelKeyPress that WebHost.Run()'s implementation uses, but personally it's just easier to use .NET Core's Hosting API and call Run() and just register your ServiceStack AppHost as a .NET Core module.

mythz
  • 141,670
  • 29
  • 246
  • 390
  • I've had a go at using the code as (I think) you have suggested - it appears to work in both Windows and in my Linux Docker... the update is to my original question - at the bottom of the post. What do you think of the solution? Have I understood you correctly? – Jay Nov 09 '16 at 13:26
  • @Jay if it works I'm sure it's fine, but why not use .NET Core's recommended hosting model? It's not clear why you'd want to maintain your own implementation. – mythz Nov 09 '16 at 13:32
  • Possibly because I don't understand it yet :) I'm a complete noob to .NET core and really don't understand how the example here: http://docs.servicestack.net/releases/v4.5.2.html#apphostbase-net-core-module will work in Linux; i.e. what does `UseIISIntegration()` do in a Linux container, and how do I set the port I want to listen on. I'm so used to creating simple self hosted console apps using ServiceStack - that I wanted something that worked pretty much the same as before. Maybe I need to do a bit more reading about .NET core and the hosting model. – Jay Nov 09 '16 at 13:37
  • @Jay All apps in .NET Core are self-hosting Console Apps and UseIISIntegration() is ignored on Linux so you can ignore/remove it. You don't need to understand .NET Core hosting model, just leave it as-is, the main thing it does is point to your custom Startup class and listen on the URL you specify. Have a look at the existing .NET Core Live Demos at https://github.com/NetCoreApps which contain small stand alone examples of ServiceStack's existing Live Demos. – mythz Nov 09 '16 at 13:46
  • 1
    @Jay also each of the .NET Core examples run as-is on Windows and Linux. They're all hosted on Linux/Docker and deployed with AWS ECS using the deploy scripts contained in each repo as documented in this step-by-step guide: http://docs.servicestack.net/deploy-netcore-docker-aws-ecs – mythz Nov 09 '16 at 13:52
  • Found exactly what I needed in the sample files - I've linked to the git source repository and updated the original question. You are right; so much cleaner using the WebHostBuilder. I think I understand now! :) – Jay Nov 09 '16 at 14:56
  • @Jay Great, Happy to hear it :) also you don't need the ServiceStack.Kestrel NuGet package anymore as it just wraps Kestrel within the AppSelfHostBase class which you're no longer using. – mythz Nov 09 '16 at 15:00