【问题标题】:Single instance dotnetcore cli app on linuxLinux 上的单实例 dotnetcore cli 应用程序
【发布时间】:2017-11-18 10:01:55
【问题描述】:

我对如何为 dotnetcore 控制台应用程序实施单一实例策略感兴趣。令我惊讶的是,似乎没有太多关于这个话题的内容。我找到了这个stacko,@ 987654321@,但它似乎不适用于我在带有ubuntu 的dotnetcore 上。这里有人做过吗?

【问题讨论】:

  • 在 macOS 上使用命名互斥锁似乎还不够(刚刚测试过)。您可以尝试使用某种 pidfile,只需要确保在主进程退出时始终删除该文件。
  • 是的,我以前想过,但我希望有更好的方法。

标签: c# ubuntu .net-core


【解决方案1】:

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

【讨论】:

    【解决方案2】:

    这是我使用 命名管道 的实现。它支持从第二个实例传递参数。

    注意:我没有在 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 确实.. 就我而言,这只是为了防止人类启动多个实例。大概可以修改为等待StartListeningServer完成,检查管道服务器是否创建成功。如果您确实走这条路,请随时修改答案:)
    【解决方案3】:

    由于 Linux/MacOS 上的互斥检查问题(如上所述),这在 .NET 核心上比应有的要困难一些。此外,Theyouthis 的解决方案也无济于事,因为所有 .NET 核心应用程序都通过 CLI 运行,该 CLI 的进程名称为“dotnet”,如果您在同一台机器上运行多个 .NET 核心应用程序,重复实例检查将错误地触发。

    执行此操作的一种简单且多平台稳健的方法是在应用程序启动时打开一个文件进行写入,并在最后关闭它。如果文件无法打开,那是由于另一个实例同时运行,您可以在 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 之外执行该应用程序时,它与可执行文件具有相同的名称。如果应用程序在没有关闭流的情况下崩溃会发生什么,它可以保持打开状态吗?
    • 是的,我正在通过 Visual Studio 进行测试,如果使用自包含应用程序运行,名称更改是正确的。此外,使应用程序崩溃 Windows 将关闭流(测试正常),但尚未在 Linux 上尝试过。
    【解决方案4】:

    @MusuNaji 解决方案的变体:How to restrict a program to a single instance

        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% 可靠的方式来执行单实例策略。
    • 我想在我的情况下实现单实例策略的最佳方法是使其成为 linux 守护进程。我认为至少对于暴发户来说,我的默认设置是单实例。
    • 我认为这不会很好,因为所有 .net 核心进程名称都是“netcore”(无论如何在 2.x 中),这是 CLI 而不是您的特定应用程序名称,这意味着任何 . NET 核心应用程序将触发进程名称的测试。
    • 更正 dotnet 核心进程名称是 dotnet 而不是 netcore。请参阅我上面的答案,以了解应该更好的简单替代方案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-09-18
    • 2017-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-24
    相关资源
    最近更新 更多