使用 C# 实现进程间通信的单例应用程序






3.72/5 (6投票s)
创建一个只能运行一次的应用程序,但可以接受来自后续运行的命令行参数
引言
有时,您可能有一个应用程序,重复启动主应用程序没有意义,但您可能希望再次运行该应用程序并传递其他命令行参数。 这样,您可以运行该应用程序,它将打开您的程序,然后您可以再次运行该应用程序,并将其他命令行信息传递给已运行的应用程序。 例如,当您希望现有应用程序从命令行打开新文件时,这非常有用。 所以它像这样烘烤出来
C:\Foo> MyApp myfile1.txt C:\Foo> MyApp myfile2.txt myfile3.txt
在这种情况下,MyApp 仅在第一次启动实际应用程序。 第二次,它检测到它已经在运行,并且只是将传入的命令行发送到已经运行的应用程序进行处理。
概念化这个混乱的局面
为了提供此功能,我们的应用程序有两个主要方面。
首先,应用程序需要检测它是否已经在运行,并且它将根据它是否已经在运行来执行不同的操作。
其次,我们需要一种让两个进程通信的方式。 在这种情况下,主应用程序将等待来自后续启动的命令行数据。
第一个方面非常简单。 我们使用命名互斥锁以防止主应用程序代码启动两次。 命名互斥锁可以被该用户的所有正在运行的进程看到。
第二个方面稍微困难一些,但幸运的是,并没有困难多少。 我们将使用 .NET 的命名管道功能来促进主应用程序(服务器)和后续启动(客户端)之间的通信。 命名管道也可以被该用户的所有正在运行的进程看到。
编写这个混乱的程序
我们正在使用的代码几乎完整地呈现在这里。 为了节省空间,我只删除了 using 指令。
class Program
{
static string _AppName=
Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().GetName().Name);
static void Main(string[] args)
{
// we need to get our app name so that
// we can create unique names for our mutex and our pipe
var notAlreadyRunning = true;
// wrap the meat of the application code with a named mutex so it runs only once
using (var mutex = new Mutex(true, _AppName + "Singleton", out notAlreadyRunning))
{
if (notAlreadyRunning)
{
// do additional work here, startup stuff
Console.WriteLine("Running. Press any key to exit...");
// ...
// now process our initial main command line
_ProcessCommandLine(args);
// start the IPC sink.
var srv = new NamedPipeServerStream(_AppName+"IPC",
PipeDirection.InOut,1,PipeTransmissionMode.Message,PipeOptions.Asynchronous);
// it's easier to use the AsyncCallback than it is to use Tasks here:
// this can't block, so some form of async is a must
srv.BeginWaitForConnection(new AsyncCallback(_ConnectionHandler), srv);
// block here until exit
Console.ReadKey();
// if this was a windows forms app you would put your
// "Applicantion.Run(new MyForm());" here
// finally, run any teardown code and exit
srv.Close();
}
else // another instance is running
{
// connect to the main app
var cli = new NamedPipeClientStream(".",_AppName + "IPC",PipeDirection.InOut);
cli.Connect();
var bf = new BinaryFormatter();
// serialize and send the command line
bf.Serialize(cli, args);
cli.Close();
// and exit
}
}
}
static void _ConnectionHandler(IAsyncResult result)
{
var srv = result.AsyncState as NamedPipeServerStream;
srv.EndWaitForConnection(result);
// we're connected, now deserialize the incoming command line
var bf = new BinaryFormatter();
var inargs = bf.Deserialize(srv) as string[];
// process incoming command line
_ProcessCommandLine(inargs);
srv.Close();
srv = new NamedPipeServerStream(_AppName + "IPC", PipeDirection.InOut,
1, PipeTransmissionMode.Message, PipeOptions.Asynchronous);
srv.BeginWaitForConnection(new AsyncCallback(_ConnectionHandler), srv);
}
static void _ProcessCommandLine(string[] args)
{
// we received some command line
// arguments.
// do actual work here
Console.Write("Command line received: ");
for(var i = 0;i<args.Length;++i)
{
Console.Write(args[i]);
Console.Write(" ");
}
Console.WriteLine();
}
}
我们这里有很多事情要做。
让我们从顶部开始,获取我们将用于标识应用程序的互斥锁和命名管道的应用程序名称。
接下来。 我们创建一个命名互斥锁,并将我们的主应用程序代码包装在这个块中。 这可以防止应用程序的主部分启动两次。
如果它尚未运行,我们首先要做的是运行任何应用程序初始化代码,然后处理任何命令行参数。 然后,我们启动基于命名管道的 IPC 服务器,该服务器注册一个回调,以便在我们从应用程序的后续实例获得连接时调用。 我们注册的回调读取传入的命令行数据,并按它们到达时处理发送的参数。 最后,我们阻止直到退出,在这种情况下使用 Console.ReadKey()
,但实际上,您会希望创建自己的退出条件。 对于 Windows 窗体应用程序,您将使用您的主应用程序窗体调用 Application.Run()
来阻止,而不是 Console.ReadKey()
。
现在,如果它已经在运行,我们所做的就是打开主应用程序实例之前创建的命名管道,并序列化我们的命令行数据,将其发送到管道中,然后我们关闭我们的管道连接并退出。
在 _ConnectionHandler()
中,我们处理每个传入的连接,从管道中读取任何命令行参数,然后我们实际重新启动命名管道服务器,因为出于某种原因,它喜欢在您结束连接后自行关闭。 我尚未确定这是设计使然,还是由于某种问题,但这种解决方法似乎还可以。
在 _ProcessCommandLine()
args 中,我们进行参数处理。 我们在演示中所做的只是将其输出到控制台,但实际上您可能希望做一些更实质性的事情,例如根据参数打开一个文件。
历史
- 2020 年 5 月 1 日 - 首次提交