在Linux上运行单个实例的Dotnet Core命令行应用程序

4

我对如何强制执行dotnetcore控制台应用程序的单实例策略感兴趣。令我惊讶的是,似乎没有太多关于这个主题的资料。我找到了这个stacko,如何限制程序只能运行一个实例,但它在我使用Ubuntu上的dotnetcore时似乎不起作用。有人在这里做过吗?


似乎在 macOS 上仅使用命名互斥量是不够的(已经测试过了)。您可以尝试使用某种 pidfile,只需要确保当主进程退出时始终删除该文件。 - dbattaglia
1
是的,我之前也考虑过那个方法,但我希望能找到更好的解决方案。 - BillHaggerty
4个回答

2

这是对 @MusuNaji 的解决方案的改进,原文链接:如何限制程序只有一个实例

    private static bool AlreadyRunning()
    {
        Process[] processes = Process.GetProcesses();
        Process currentProc = Process.GetCurrentProcess();
        logger.LogDebug("Current proccess: {0}", currentProc.ProcessName);
        foreach (Process process in processes)
        {
            if (currentProc.ProcessName == process.ProcessName && currentProc.Id != process.Id)
            {
                logger.LogInformation("Another instance of this process is already running: {pid}", process.Id);
                return true;
            }
        }
        return false;
    }

现在我想起来,如果您的进程名称不唯一,这将无法很好地工作。因此,这是解决方案的先决条件。仍然开放接受100%可靠的实施单个实例策略的方法。 - BillHaggerty
我想在我的情况下实现单个实例策略的最佳方法应该是将其作为Linux守护程序。我认为至少使用upstart,默认情况下会强制执行单个实例。 - BillHaggerty
1
我认为这不会很好地工作,因为所有的 .NET Core 进程名称都是“netcore”(至少在 2.x 版本中),这是 CLI 而不是您特定的应用程序名称,这意味着任何 .NET Core 应用程序都将触发进程名称的测试。 - deandob
1
纠正一下,dotnet core的进程名称是dotnet而不是netcore。请参考我上面的答案,那里有一个更好的简单替代方案。 - deandob

1
这在.NET Core上比应该更加困难,因为Linux/MacOS上的互斥检查问题(如上所述)。此外,Theyouthis的解决方案并不实用,因为所有.NET Core应用程序都是通过名为“dotnet”的CLI运行的进程运行的,如果您在同一台机器上运行多个.NET Core应用程序,则重复实例检查将不正确触发。 一个简单的方法是,在应用程序启动时打开文件进行写操作,并在结束时关闭它。如果文件无法打开,则由于另一个实例正在同时运行,您可以在try/catch中处理它。使用FileStream打开文件还将在首次不存在时创建它。
            try
            {
                lockFile = File.OpenWrite("SingleInstance.lck");
            }
            catch (Exception)
            {
                Console.WriteLine("ERROR - Server is already running. End that instance before re-running. Exiting in 5 seconds...");
                System.Threading.Thread.Sleep(5000);
                return;
            }

你错误地断言所有的netcore应用都是通过dotnet CLI运行的,虽然你指出从CLI运行不会正确地处理我的解决方案是好的。当你构建一个自包含的应用并在dotnet CLI之外执行应用程序时,它与可执行文件具有相同的名称。如果应用程序崩溃而没有关闭流,会发生什么,它能保持打开状态吗? - BillHaggerty
是的,我正在使用Visual Studio进行测试,如果使用自包含的应用程序运行,名称会改变,你是正确的。此外,崩溃的应用程序Windows将关闭流(测试OK),但尚未在Linux上尝试过这种情况。 - deandob

1
这是我的实现,使用了命名管道。它支持从第二个实例传递参数。 < p > 注意:我没有在Linux或Mac上测试,但理论上应该可以工作。

用法


        public static int Main(string[] args)
        {
            instanceManager = new SingleInstanceManager("8A3B7DE2-6AB4-4983-BBC0-DF985AB56703");
            if (!instanceManager.Start())
            {
                return 0; // exit, if same app is running
            }
            instanceManager.SecondInstanceLaunched += InstanceManager_SecondInstanceLaunched;
            // Initialize app. Below is an example in WPF.
            app = new App();
            app.InitializeComponent();
            return app.Run();
        }
        private static void InstanceManager_SecondInstanceLaunched(object sender, SecondInstanceLaunchedEventArgs e)
        {
            app.Dispatcher.Invoke(() => new MainWindow().Show());
        }

您的复制粘贴代码


    public class SingleInstanceManager
    {
        private readonly string applicationId;
        public SingleInstanceManager(string applicationId)
        {
            this.applicationId = applicationId;
        }
        /// <summary>
        /// Detect if this is the first instance. If it is, start a named pipe server to listen for subsequent instances. Otherwise, send <see cref="Environment.GetCommandLineArgs()"/> to the first instance.
        /// </summary>
        /// <returns>True if this is tthe first instance. Otherwise, false.</returns>
        public bool Start()
        {
            using var client = new NamedPipeClientStream(applicationId);
            try
            {
                client.Connect(0);
            }
            catch (TimeoutException)
            {
                Task.Run(() => StartListeningServer());
                return true;
            }
            var args = Environment.GetCommandLineArgs();
            using (var writer = new BinaryWriter(client, Encoding.UTF8))
            {
                writer.Write(args.Length);
                for (int i = 0; i < args.Length; i++)
                {
                    writer.Write(args[i]);
                }
            }
            return false;
        }
        private void StartListeningServer()
        {
            var server = new NamedPipeServerStream(applicationId);
            server.WaitForConnection();
            using (var reader = new BinaryReader(server, Encoding.UTF8))
            {
                var argc = reader.ReadInt32();
                var args = new string[argc];
                for (int i = 0; i < argc; i++)
                {
                    args[i] = reader.ReadString();
                }
                SecondInstanceLaunched?.Invoke(this, new SecondInstanceLaunchedEventArgs { Arguments = args });
            }
            StartListeningServer();
        }
        public event EventHandler<SecondInstanceLaunchedEventArgs> SecondInstanceLaunched;
    }
    public class SecondInstanceLaunchedEventArgs
    {
        public string[] Arguments { get; set; }
    }

单元测试

[TestClass]
    public class SingleInstanceManagerTests
    {
        [TestMethod]
        public void SingleInstanceManagerTest()
        {
            var id = Guid.NewGuid().ToString();
            var manager = new SingleInstanceManager(id);
            string[] receivedArguments = null;
            var correctArgCount = Environment.GetCommandLineArgs().Length;
            manager.SecondInstanceLaunched += (sender, e) => receivedArguments = e.Arguments;
            var instance1 = manager.Start();
            Thread.Sleep(200);
            var manager2 = new SingleInstanceManager(id);
            Assert.IsFalse(manager2.Start());
            Thread.Sleep(200);
            Assert.IsTrue(instance1);
            Assert.IsNotNull(receivedArguments);
            Assert.AreEqual(correctArgCount, receivedArguments.Length);
            var receivedArguments2 = receivedArguments;
            var manager3 = new SingleInstanceManager(id);
            Thread.Sleep(200);
            Assert.IsFalse(manager3.Start());
            Assert.AreNotSame(receivedArguments, receivedArguments2);
            Assert.AreEqual(correctArgCount, receivedArguments.Length);
        }
    }

这不是原子解决方案。在其他应用程序开始侦听第一个实例之前,仍然可以运行多个实例。当我测试它时,一次启动了1000个实例。其中500个能够在其他实例发现某些实例已经在运行之前启动。 - Michał Jankowski
@MichałJankowski 确实如此。在我的情况下,这只是为了防止人类启动多个实例。它可能可以修改为等待StartListeningServer完成,并检查管道服务器是否成功创建。如果您选择这条路线,请随意修改答案 :) - fjch1997

0
deandob的解决方案的缺点是,用户可以从其他路径启动应用程序。因此,您可能更喜欢为所有用户使用一些静态路径或tmp路径。
//second instance launch guard
var tempPath = Environment.GetEnvironmentVariable("TEMP", EnvironmentVariableTarget.Machine) 
           ?? 
           Path.GetTempPath();
var lockPath = Path.Combine(tempPath, "SingleInstance.lock");
await using var lockFile = File.OpenWrite(lockPath);

在这里,我正在尝试在机器范围内获取TEMP系统变量(而不是用户TEMP),如果它为空,则回退到Windows上用户的临时文件夹或某些Linux上共享的/tmp


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接