Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed typos and capitalization and improved XML doc comments #78

Merged
merged 2 commits into from
Mar 3, 2019

Conversation

jnm2
Copy link
Contributor

@jnm2 jnm2 commented Mar 3, 2019

Hope this is helpful. I love the project!

@dasMulli dasMulli merged commit f925fb2 into dasMulli:master Mar 3, 2019
@dasMulli
Copy link
Owner

dasMulli commented Mar 3, 2019

Thanks!

Out of interest: which part are you using? managing services / service host / both?

@jnm2
Copy link
Contributor Author

jnm2 commented Mar 3, 2019

@dasMulli Both for sure. I'm still getting acquainted with it.

I want my entire service definition to look something like this:

using System.Threading;
using System.Threading.Tasks;

public sealed class Program
{
    public static Task<int> Main(string[] args)
    {
        return StandardServiceApp.MainAsync(args, RunAsync, serviceName: typeof(Program).Assembly.GetAssemblyTitle());
    }

    public static async Task RunAsync(CancellationToken stopToken)
    {
        while (true)
        {
            await Task.Delay(2000, stopToken).ConfigureAwait(false);
            System.IO.File.WriteAllText(@"C:\Users\Joseph\Desktop\Test.txt", System.DateTime.Now.ToString());
        }
    }
}

My current WIP implementation, using Microsoft's new System.CommandLine to handle the CLI argument parsing and to inject a CancellationToken that responds to Ctrl+C when run interactively:

using DasMulli.Win32.ServiceUtils;
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;

public static class StandardServiceApp
{
    public static Task<int> MainAsync(string[] args, Func<CancellationToken, Task> runAsync, string serviceName)
    {
        if (runAsync is null)
            throw new ArgumentNullException(nameof(runAsync));

        if (string.IsNullOrWhiteSpace(serviceName))
            throw new ArgumentException("Service name must be specified.", nameof(serviceName));

        var root = new RootCommand(serviceName, handler: CreateServiceRunCommandHandler(runAsync, serviceName))
        {
            new Command("run", "Runs the service interactively in the current console window.", handler: CreateInteractiveRunCommandHandler(runAsync)),

            new Command("install", "Installs the service as a Windows service.", handler: CreateInstallCommandHandler(serviceName)),

            new Command("uninstall", "Uninstalls the service as a Windows service.", handler: CreateUninstallCommandHandler(serviceName))
        };

        return root.InvokeAsync(args);
    }

    private static ICommandHandler CreateServiceRunCommandHandler(Func<CancellationToken, Task> runAsync, string serviceName)
    {
        return CommandHandler.Create((IConsole console) =>
        {
            const int ERROR_FAILED_SERVICE_CONTROLLER_CONNECT = 1063;

            try
            {
                return new Win32ServiceHost(new Service(runAsync, serviceName)).Run();
            }
            catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT)
            {
                console.Out.WriteLine("Error: " + ex.Message);
                console.Out.WriteLine("Use --help to see available commands, such as running the service interactively in the current console window.");
                return 1;
            }
        });
    }

    private static ICommandHandler CreateInteractiveRunCommandHandler(Func<CancellationToken, Task> runAsync)
    {
        return CommandHandler.Create(async (IConsole console, CancellationToken cancellationToken) =>
        {
            console.Out.WriteLine("Service is running interactively. Press Ctrl+C to stop.");

            await runAsync.Invoke(cancellationToken).TreatCancellationAsSuccess();

            if (cancellationToken.IsCancellationRequested)
                console.Out.WriteLine("Stopped in response to Ctrl+C.");
        });
    }

    private static ICommandHandler CreateInstallCommandHandler(string serviceName)
    {
        return CommandHandler.Create((IConsole console) =>
        {
            throw new NotImplementedException();

            console.Out.WriteLine("The service was successfully added.");
        });
    }

    private static ICommandHandler CreateUninstallCommandHandler(string serviceName)
    {
        return CommandHandler.Create((IConsole console) =>
        {
            new Win32ServiceManager().DeleteService(serviceName);
            console.Out.WriteLine("The service was successfully removed.");
        });
    }

    private sealed class Service : IWin32Service
    {
        private readonly Func<CancellationToken, Task> runAsync;
        private CancellationTokenSource stopSource;

        public Service(Func<CancellationToken, Task> runAsync, string serviceName)
        {
            this.runAsync = runAsync;
            ServiceName = serviceName;
        }

        public string ServiceName { get; }

        public void Start(string[] startupArguments, ServiceStoppedCallback serviceStoppedCallback)
        {
            stopSource = new CancellationTokenSource();

            runAsync.Invoke(stopSource.Token).ContinueWith(
                _ => serviceStoppedCallback.Invoke(),
                TaskContinuationOptions.ExecuteSynchronously);
        }

        public void Stop()
        {
            stopSource.Cancel();
        }
    }
}

I was going to suggest shipping something like my StandardServiceApp class if I ended up with something I like. I haven't gotten to fully test it yet. Got sidetracked by the typos! :D

@dasMulli
Copy link
Owner

dasMulli commented Mar 4, 2019

A StandardService would be kind of what #60 would be about but since I figured a good part of this lib's users depend on this library transitively it wouldn't be much value to make a general-purpose abstraction. Also, whenever I used this lib for projects, I ended up heavily customising the sample code beyond recognition..

@dasMulli dasMulli added this to the 1.3.0 milestone Mar 4, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants