3
头图

Not long ago, when trying to use C# to solve the problem of multi-opening of PC-side programs, I found that the WinForm program of VB.NET provides a very simple implementation method:


No code required, just a tick.

I was intrigued by how it was implemented, so I explored it. Today, through this article, I will introduce to you how Microsoft solves the problem of multi-opening of PC-side programs, and how to achieve the same function in C#.

Principle 1 - WindowsFormsApplicationBase base class

Compile a VB.NET WinForm program, decompile the source code, and find the entry Main method:

The entry class MyApplication inherits from the WindowsFormsApplicationBase base class, and actually executes the Run method of the base class. The Run method uses the IsSingleInstance property internally to determine whether to enable a single instance application:

The IsSingleInstance property is assigned in the constructor of the entry class:

Realize 1

By referencing the NuGet package Microsoft.VisualBasic, we can also inherit the WindowsFormsApplicationBase base class in C#, assign values to the IsSingleInstance property, and implement a single instance application:

 class Program :  WindowsFormsApplicationBase
{
    public Program()
    {
        IsSingleInstance = true;
    }
​
    protected override void OnCreateMainForm()
    {
        MainForm = new Form1();
    } 
​
    [STAThread]
    static void Main(string[] args)
    {
        new Program().Run(args);
    }
}

Although the above implementation is very simple, it is only suitable for WinForm applications, and also needs to refer to the Microsoft.VisualBasic class library.

So, we decided to dig a little deeper to see how it was implemented.

Principle 2 - Named Pipes

By looking at the Run method implementation of WindowsFormsApplicationBase (the code is abridged):

 Public Sub Run(commandLine As String())
    If Not IsSingleInstance Then
        DoApplicationModel()
    Else
        ' This is a Single-Instance application
        Dim pipeServer As NamedPipeServerStream = Nothing
        If TryCreatePipeServer(ApplicationInstanceID, pipeServer) Then
            ' --- This is the first instance of a single-instance application to run.
            Using pipeServer
                WaitForClientConnectionsAsync(pipeServer, AddressOf OnStartupNextInstanceMarshallingAdaptor, cancellationToken:=tokenSource.Token)
                DoApplicationModel()
            End Using
        Else
            Dim awaitable = SendSecondInstanceArgsAsync(ApplicationInstanceID, commandLine, cancellationToken:=tokenSource.Token).ConfigureAwait(False)
            awaitable.GetAwaiter().GetResult()
        End If
    End If 'Single-Instance application
End Sub

It can be analyzed that the internal implementation principle of Microsoft to solve the problem of multi-opening of PC-side programs is as follows:

  1. Create a NamedPipeServerStream named pipe server instance
  2. If the creation is successful, use WaitForClientConnectionsAsync to wait for the second application instance to connect
  3. If the creation fails, use SendSecondInstanceArgsAsync to send data to the first application instance

Named pipes provide interprocess communication between a pipe server and one or more pipe clients. Named pipes can be unidirectional or bidirectional. They support message-based communication and allow multiple clients to connect to the server process simultaneously using the same pipe name.

For detailed usage instructions, please refer to the official document "Using Named Pipes for Network Interprocess Communication" [1]

realization 2

Below, we use the console program to demonstrate how to implement a single instance application:

 const string pipeName = "MyIO";
const PipeOptions NamedPipeOptions = PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly;
​
static async Task Main(string[] args)
{
    try
    {
        using (var pipeServer = new NamedPipeServerStream(
                pipeName: pipeName,
                direction: PipeDirection.In,
                maxNumberOfServerInstances: 1,
                transmissionMode: PipeTransmissionMode.Byte,
                options: NamedPipeOptions))
        {
            WaitForClientConnectionsAsync(pipeServer,str => Console.WriteLine(str));
​
            Console.WriteLine($"start server {args[0]}");
            Console.ReadKey();
        }
    }
    catch
    {
        await SendSecondInstanceArgsAsync(()=> $"call from {args[0]}").ConfigureAwait(false);
    }
}

It should be noted that WaitForClientConnectionsAsync cannot add await, otherwise the subsequent code cannot be executed.

▌WaitForClientConnectionsAsync method implementation

The implementation code is as follows:

 private static async Task WaitForClientConnectionsAsync(NamedPipeServerStream pipeServer, Action<string> callback)
{
    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    while (true)
    {
        await pipeServer.WaitForConnectionAsync(cancellationTokenSource.Token).ConfigureAwait(false);
​
        try
        {
            const int bufferLength = 1024;
            var buffer = new byte[bufferLength];
            using (var stream = new MemoryStream())
            {
                while (true)
                {
                    var bytesRead = await pipeServer.ReadAsync(buffer.AsMemory(0, bufferLength), cancellationTokenSource.Token).ConfigureAwait(false);
                    if (bytesRead == 0)
                    {
                        break;
                    }
                    stream.Write(buffer, 0, bytesRead);
                }
​
                stream.Seek(0, SeekOrigin.Begin);
​
                callback(Encoding.UTF8.GetString(stream.ToArray()));
            }
        }
        finally
        {
            pipeServer.Disconnect();
        }
    }
}

▌SendSecondInstanceArgsAsync method implementation

The implementation code is as follows:

 ​private static async Task SendSecondInstanceArgsAsync(Func<string> func)
{
    using (var pipeClient = new NamedPipeClientStream(
        serverName: ".",
        pipeName: pipeName,
        direction: PipeDirection.Out,
        options: NamedPipeOptions))
    {
        CancellationTokenSource cancellationTokenSource2 = new CancellationTokenSource();
        cancellationTokenSource2.CancelAfter(2500);
​
        await pipeClient.ConnectAsync(cancellationTokenSource2.Token).ConfigureAwait(false);
​
        await pipeClient.WriteAsync(Encoding.UTF8.GetBytes(func()), cancellationTokenSource2.Token).ConfigureAwait(false);
    }
}

Demo

Create a multi-open script:

 start " " "ConsoleApp1.exe" firstInstance
​
start " " "ConsoleApp1.exe" secondInstance
​
start " " "ConsoleApp1.exe" thirdInstance

After execution, we found that the program can only be opened once. And received data from other multi-development applications:


Microsoft Most Valuable Professional (MVP)


The Microsoft Most Valuable Professional is a global award given to third-party technology professionals by Microsoft Corporation. For 29 years, technology community leaders around the world have received this award for sharing their expertise and experience in technology communities both online and offline.

MVPs are a carefully selected team of experts who represent the most skilled and intelligent people, passionate and helpful experts who are deeply invested in the community. MVP is committed to helping others and maximizing the use of Microsoft technologies by Microsoft technical community users by speaking, forum Q&A, creating websites, writing blogs, sharing videos, open source projects, organizing conferences, etc.

For more details, please visit the official website:
https://mvp.microsoft.com/en-us


Long press to identify the QR code and follow Microsoft China MSDN


微软技术栈
423 声望996 粉丝

微软技术生态官方平台。予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。