在 Avalonia 的窗口 OnClosing 事件里面,将 WindowClosingEventArgs 的 Cancel 属性赋值为 true 用来取消窗口关闭,此时 Linux 麒麟系统的 ukui 组件在进行关机时,将会在调用 会话管理器 时,收到会话管理器取消了本次操作,进而无法进行关机或注销或重启

最简复现代码如下

public partial class MainWindow : Window
{
    protected override void OnClosing(WindowClosingEventArgs e)
    {
        e.Cancel = true;
        base.OnClosing(e);
    }

    ... // 忽略其他代码
}

使用以上代码之后,能够在麒麟带 ukui 桌面环境的设备上,复现出从开始菜单点关机或注销或重启失败问题,且此时通知栏收到会话管理器取消了本次操作的提示。此时依然可以从命令行用 shutdown 命令进行关闭

解决方法是判断当前的关闭原因,如果是应用关闭或系统关闭的话,就不阻止窗口关闭即可,代码如下

public partial class MainWindow : Window
{
    protected override void OnClosing(WindowClosingEventArgs e)
    {
        if (e.CloseReason is WindowCloseReason.ApplicationShutdown or WindowCloseReason.OSShutdown)
        {
            // 应用关闭或操作系统关闭时,允许关闭窗口
            // 为什么需要判断 ApplicationShutdown 而不是只判断 OSShutdown 呢?这是因为在 11.3.1 及以前版本,在 Avalonia 底层挖了一个坑,无法区分原因。我将此问题报告给官方,详细请看
            // [Incorrect WindowCloseReason Passed During System Shutdown in Avalonia · Issue #19027 · AvaloniaUI/Avalonia](https://github.com/AvaloniaUI/Avalonia/issues/19027 )
        }
        else
        {
            e.Cancel = true;
        }

        base.OnClosing(e);
    }

    ... // 忽略其他代码
}

详细机制原因如下

在 Linux 里,有一个机制叫 xsm (session manager) 会话管理器机制。详细请参阅 https://linux.die.net/man/1/xsm

在 Avalonia 里面,通过 libSM.so.6 库和 xsm 建立联系。通过 SmcOpenConnection 机制建立联系。当系统进行关机或注销或重启时,会进入 SaveYourself 机制回调。在 SaveYourself 里面,通过 libSM 的 SmcInteractRequest 方法传入了 Avalonia 的处理逻辑

在 Avalonia 的处理逻辑里面,先判断了 AVALONIA_X11_USE_SESSION_MANAGEMENT 环境变量是否被设置为 0 的值,如果被设置了,则直接退出,不触发业务上的逻辑。这就意味着,如果想要调查是否 Avalonia 应用导致 Linux 麒麟系统无法关机或注销或重启时,可以通过 export AVALONIA_X11_USE_SESSION_MANAGEMENT=0 设置环境变量再启动应用来进行测试

再接着触发 ShutdownRequestedEventArgs 参数的 ShutdownRequested 事件。这将在 ClassicDesktopStyleApplicationLifetime 里面进行处理。处理方法是遍历当前所有窗口,尝试将其关闭,如果依然还有存活的窗口,则设置取消应用退出。核心代码如下

        private bool DoShutdown(
            ShutdownRequestedEventArgs e,
            bool isProgrammatic,
            bool force = false,
            int exitCode = 0)
        {
            if (!force)
            {
                ShutdownRequested?.Invoke(this, e);

                if (e.Cancel)
                    return false;

                if (_isShuttingDown)
                    throw new InvalidOperationException("Application is already shutting down.");
            }

            _exitCode = exitCode;
            _isShuttingDown = true;
            var shutdownCancelled = false;

            try
            {
                // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
                // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
                // owners.
                foreach (var w in new List<Window>(_windows))
                {
                    if (w.Owner is null)
                    {
                        var ignoreCancel = force || (ShutdownMode == ShutdownMode.OnMainWindowClose && w != MainWindow);
                        w.CloseCore(WindowCloseReason.ApplicationShutdown, isProgrammatic, ignoreCancel);
                    }
                }

                if (!force && Windows.Count > 0)
                {
                    e.Cancel = true; // 核心阻止关闭的代码
                    shutdownCancelled = true;
                    return false;
                }

                var args = new ControlledApplicationLifetimeExitEventArgs(exitCode);
                Exit?.Invoke(this, args);
                _exitCode = args.ApplicationExitCode;                
            }
            finally
            {
                _isShuttingDown = false;

                if (!shutdownCancelled)
                {
                    _cts?.Cancel();
                    _cts = null;
                    Dispatcher.UIThread.InvokeShutdown();
                }
            }

            return true;
        }

当 ShutdownRequestedEventArgs 的 Cancel 被设置为 true 时,将传递到 libSM 的 SmcInteractDone 方法,从而通知会话管理器取消了本次操作。代码如下

                var e = new ShutdownRequestedEventArgs();

                if (_platform.Options.EnableSessionManagement)
                {
                    ShutdownRequested?.Invoke(this, e);
                }

                SMLib.SmcInteractDone(smcConn, e.Cancel);

                if (e.Cancel)
                {
                    return;
                }

                _saveYourselfPhase = false;

                SMLib.SmcSaveYourselfDone(smcConn, true);

https://www.x.org/releases/X11R7.7/doc/libSM/SMlib.html 文档所示,调用的 SmcInteractDone 方法的签名如下

void SmcInteractDone(SmcConn smc_conn, Bool cancel_shutdown);

After interacting with the user (in response to an “Interact” message), you should call SmcInteractDone

smc_conn:The session management connection object.

cancel_shutdown:If True, indicates that the user requests that the entire shutdown be cancelled.

cancel_shutdown:如果是 True 值,表示用户请求取消整个关闭操作

这就意味着只要有窗口没有关闭,则会阻止系统关闭

在麒麟 ukui 底层里面,就依赖了 xsm 机制。当收到了有至少一个应用取消关闭时,将会取消关机等的令牌,从而取消关机等命令

由于 ukui 和 Windows 系统的机制不同,不会再弹出提示窗口说有哪些应用没有退出,是否强制关机。因此会导致用户产生困扰,用户将无法从开始菜单进行关机。我认为更好的解决方法是让 ukui 仿照 Windows 系统的做法,弹出提示窗口,如果用户没有主动取消关机,则等几秒强制关机


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/Avalonia-%E5%8F%96%E6%B6%88%E7%AA%97%E5%8F%A3%E5%85%B3%E9%97%AD%E5%AF%BC%E8%87%B4-Linux-%E9%BA%92%E9%BA%9F%E7%B3%BB%E7%BB%9F%E6%97%A0%E6%B3%95%E5%85%B3%E6%9C%BA%E6%B3%A8%E9%94%80%E9%87%8D%E5%90%AF.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利