Runtime: Exposing plumbing for single instance application

Created on 5 May 2020  路  17Comments  路  Source: dotnet/runtime

Microsoft.VisualBasic.dll provided various services for applications, including single-instance application which also handles forwarding command line arguments from the first instance to the second. At a minimum, we'd like to expose this functionality directly from WinFroms (https://github.com/dotnet/winforms/issues/3131), without the need to reference Microsoft.VisualBasic assembly. However, it seems likely that we'd like to expose similar functionality in other application models as well:

  • Console apps
  • WPF
  • Services/daemons
  • ...

Since each application model likely already has an entry point (for example, both WinForms and WPF have an Application type that would expose properties/events) we don't need a super convenient API in the BCL, but we do want an API in order to encapsulate the core functionality in a single place. This also allows 3rd parties to use this as plumbing as well.

API Proposal

```C#
namespace System.IO
{
public static class SingleInstanceApp
{
///


/// Create a named pipe from the first application instance.
/// Returns null if pipe already exists (and the application is not the first instance).
///

public static NamedPipeServerStream CreatePipe(string pipeName);

    /// <summary>
    /// Handle subsequent application instance notifications. Invoke callback with
    /// each subsequent instance command-line arguments
    /// </summary>
    public static void WaitForSubsequentInstancesAsync(NamedPipeServerStream pipeServer, Action<string[]> callback);

    /// <summary>
    /// Notify first application instance of subsequent instance command-line.
    /// Invoked if CreatePipe() fails.
    /// Returns true if succeeded.
    /// </summary>
    public static bool SendSubsequentInstanceArgs(string pipeName, TimeSpan timeout, string[] args);
}

}

### Usage

```C#
static void Main(string args[])
{
    var appId = "dff76cd2-4aeb-4779-801a-7e77149dabfd";
    using NamedPipeServerStream pipeStream = SingleInstanceApp.CreatePipe(appId);

    if (pipeStream == null)
    {
        SingleInstanceApp.SendSubsequentInstanceArgs(appId, Timeout.InfiniteTimeSpan, args);
        return;
    }

    SingleInstanceApp.WaitForSubsequentInstancesAsync(pipeStream, otherArgs => {
        Console.WriteLine("Another instance sent us this:");

        foreach (var arg in otherArgs)
            Console.WriteLine(arg);
    });

    Console.ReadLine();
}

@bradygaster @cartermp @cston @diverdan92 @jaredpar @KathleenDollard @KlausLoeffelmann @merriemcgaw @OliaG @RussKie @shirhatti @timheuer

api-needs-work area-System.IO

Most helpful comment

we shouldn't tie such an API to NamedPipeServerStream. That would be an implementation detail.

Perhaps:
```C#
class SingleInstanceApp : IDisposable
{
///


/// Create a handler for single instance behavior.
/// Returns null if this is not the first instance.
///

public static SingleInstanceApp CreateHandler(string applicationId);

/// <summary>
/// Handle subsequent application instance notifications. Invoke callback with
/// each subsequent instance command-line arguments
/// </summary>
public void OnSubsequentInstances(Action<string[]> callback);

// ...

}
```

All 17 comments

Tagging subscribers to this area: @jozkee
Notify danmosemsft if you want to be subscribed.

Personally I feel the API is unnecessary verbose, but I struggle to provide any alternative at this stage.
Specifically I'm finding SendSubsequentInstanceArgs confusing, I had to re-read the sample few times to grasp this will be sending args _from_ the second instance _to_ the first instance. I can imagine how users who may not have a good command of English (like me atm :)) may be struggling with comprehension.

Few other points:

  1. This contradicts the established naming convention for async API:

    public static void WaitForSubsequentInstancesAsync(NamedPipeServerStream pipeServer, Action<string[]> callback);
    

    Since this is an Async method, perhaps it should have the following signature, though I'm not sure what it would mean for an app flow:

    public static Task WaitForSubsequentInstancesAsync(NamedPipeServerStream pipeServer, Action<string[]> callback, CancellationToken cancellationToken);
    
  2. Likewise for the other API, should they be made async too?

  3. Perhaps instead of timeout in SendSubsequentInstanceArgs use CancellationToken?

This sounds very Windows specific and full of problems if extended beyond Win32 applications.

  • What is an example of console app or service/daemon that would actually want to use this?
  • What are the security properties of this solution?
  • How does it handle situation where one instance of the application is launching while the other instance is exiting?

WaitForSubsequentInstancesAsync takes a callback and doesn't return task-like object.
May be OnSubsequentInstances?

In addition to all the feedback thus far, we shouldn't tie such an API to NamedPipeServerStream. That would be an implementation detail.

Maybe this API should be modeled on something like the Android activities model with less emphasis on Main. As is, the proposed API doesn't really make sense on mobile.

we shouldn't tie such an API to NamedPipeServerStream. That would be an implementation detail.

Perhaps:
```C#
class SingleInstanceApp : IDisposable
{
///


/// Create a handler for single instance behavior.
/// Returns null if this is not the first instance.
///

public static SingleInstanceApp CreateHandler(string applicationId);

/// <summary>
/// Handle subsequent application instance notifications. Invoke callback with
/// each subsequent instance command-line arguments
/// </summary>
public void OnSubsequentInstances(Action<string[]> callback);

// ...

}
```

I have prototyped a solution and @cston has pick up the task with a slightly different implementation. This needs to be compatible with the existing VB Framework implementation which uses facilities that are not available in Core. One key point is applicationId has a specific implementation that yields a specific behavior. VB and I have used this for services in the past, not sure how it would be used for a console app but with Async Main it might be interesting. It should be implementable on any OS that has an RCP or inter-process file communication system. Since it is local to a single user, security should not be an issue. The 2 current prototypes both deal with the issue of one app exiting while another is starting using Mutex and/Or namedPipes. The original VB implementation was very robust as is the one used in Office and both have existed a very, very long time. In VB the applicationId is not a GUID, it starts as a fixed GUID created when the project is first created by Visual Studio and stored in a file and never changes. To that the projects Major and Minor version are appended this was handled by API's/Visual Studio support that are not part of Core. So 2 applications are deemed to be the same even if the 3rd or 4th version field changes. It was recommended but not implemented in Framework to allow the formula for applicationId to be Overridable. I don't know why it does not make sense for mobile, Office Apps today use this model and they run on mobile devices, trying to open a new instance of the app causes the startup args to be sent first instance and it can do what it wants, the 2nd-N instances exit after sending the args. If you are the first instance it makes no sense to cancel/timeout the connection, the comm channel will close when the first instance exits and the next instance to start becomes the first instance.

Office Apps today use this model

FWIW, Office Apps implement this model by sending Windows messages (SendMessageTimeout) .

mobile devices

The app model on mobile devices is very difference as @lambdageek pointed out. The user experience may be similar, but the actual implementation is very different.

@lambdageek

Maybe this API should be modeled on something like the Android activities model with less emphasis on Main. As is, the proposed API doesn't really make sense on mobile.

I don't think the intent of this API is that it's used directly by applications. Rather, it's plumbing for application models to provide an API that feels natural.

@cston What is the approach to determining if an app is the same app in your .NET Core implementation? Are versions considered?

What is the approach to determining if an app is the same app in your .NET Core implementation? Are versions considered?

For now, I ignored version and simply used ModuleVersionId of the main module as the applicationId passed to the SingleInstanceApp methods. (Other users of SingleInstanceApp could use a different approach for calculating an applicationId.)

This may be more of a question for the VB code that sits on top of this, but how would they if GetApplicationInstanceID is not Overridable and there is no per application GUID created by Visual Studio, as it is today in Framework, for Core apps? The VB prototype currently uses ManifestModule.ModuleVersionId which changes with every build and is different between Debug and Release builds even with no changes in any code.

What is an example of console app or service/daemon that would actually want to use this?

ShareX could use this. Today it uses remoting, making porting to .NET 5 a bit of a hassle. I tried migrating to a named pipe, but it caused bugs so we had to revert for now.

While the way SingleInstance is calculated is not public in Preview 6 it is based on a GUID that the developer controls. Any application that shares that GUID and the same same Major and Minor version are considered the same app. I was able, for example, to send a message to one app from another using this. I agree that making GetApplicationInstanceID public overridable could be an improvement if one wanted to use SingleInstance for other features. The one I have seen in demos is the ability of a update utility to communicate with a server telling it to gracefully shut down in a standard way. One issue that would arise is today if two apps are deemed to be the same the second always exists and that would not be desirable in the example above. @sylveon The implementation is SingleInstance in Preview 6 uses Pipes and has proven to be very usable you might want to compare what you tried to that, there were a lot of corner cases it had to deal with.

Is there a built in assumption that when the second instance tries to start and finds a first instance it automatically exits? While that is the VB behavior I have seen demos/questions where people don't want the second application to exit immediately. The args actually request the first app exit possibly so a newer version can take over (similar to what happens with Mobile apps from a user perspective), also as stated above the second app might want to do something after the first exits.

Issue with the API: The name applicationId should be changed to instanceID at least in VB it is not just a GUID (and string allows for that) and the different versions of the application have the same ID today.

Video

  • The namespace is an odd choice
  • It seems folks in this thread have concerns with the shape.
  • @KathleenDollard @cston if this is something we want to pursue for .NET 6/MAUI we should sit down and design this a bit further.
Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamesqo picture jamesqo  路  3Comments

omariom picture omariom  路  3Comments

EgorBo picture EgorBo  路  3Comments

matty-hall picture matty-hall  路  3Comments

noahfalk picture noahfalk  路  3Comments