寒江钓雪十三郎 發表於 2025-6-12 07:21:00

Avalonia 取消窗口关闭导致 Linux 麒麟系统无法关机注销重启

<p>最简复现代码如下</p>
<pre><code class="language-csharp">public partial class MainWindow : Window
{
    protected override void OnClosing(WindowClosingEventArgs e)
    {
      e.Cancel = true;
      base.OnClosing(e);
    }

    ... // 忽略其他代码
}
</code></pre>
<p>使用以上代码之后,能够在麒麟带 ukui 桌面环境的设备上,复现出从开始菜单点关机或注销或重启失败问题,且此时通知栏收到会话管理器取消了本次操作的提示。此时依然可以从命令行用 shutdown 命令进行关闭</p>
<p>解决方法是判断当前的关闭原因,如果是应用关闭或系统关闭的话,就不阻止窗口关闭即可,代码如下</p>
<pre><code class="language-csharp">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 底层挖了一个坑,无法区分原因。我将此问题报告给官方,详细请看
            // (https://github.com/AvaloniaUI/Avalonia/issues/19027 )
      }
      else
      {
            e.Cancel = true;
      }

      base.OnClosing(e);
    }

    ... // 忽略其他代码
}
</code></pre>
<p>详细机制原因如下</p>
<p>在 Linux 里,有一个机制叫 xsm (session manager) 会话管理器机制。详细请参阅 https://linux.die.net/man/1/xsm</p>
<p>在 Avalonia 里面,通过 <code>libSM.so.6</code> 库和 xsm 建立联系。通过 <code>SmcOpenConnection</code> 机制建立联系。当系统进行关机或注销或重启时,会进入 SaveYourself 机制回调。在 SaveYourself 里面,通过 libSM 的 SmcInteractRequest 方法传入了 Avalonia 的处理逻辑</p>
<p>在 Avalonia 的处理逻辑里面,先判断了 <code>AVALONIA_X11_USE_SESSION_MANAGEMENT</code> 环境变量是否被设置为 0 的值,如果被设置了,则直接退出,不触发业务上的逻辑。这就意味着,如果想要调查是否 Avalonia 应用导致 Linux 麒麟系统无法关机或注销或重启时,可以通过 <code>export AVALONIA_X11_USE_SESSION_MANAGEMENT=0</code> 设置环境变量再启动应用来进行测试</p>
<p>再接着触发 ShutdownRequestedEventArgs 参数的 ShutdownRequested 事件。这将在 ClassicDesktopStyleApplicationLifetime 里面进行处理。处理方法是遍历当前所有窗口,尝试将其关闭,如果依然还有存活的窗口,则设置取消应用退出。核心代码如下</p>
<pre><code class="language-csharp">      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&lt;Window&gt;(_windows))
                {
                  if (w.Owner is null)
                  {
                        var ignoreCancel = force || (ShutdownMode == ShutdownMode.OnMainWindowClose &amp;&amp; w != MainWindow);
                        w.CloseCore(WindowCloseReason.ApplicationShutdown, isProgrammatic, ignoreCancel);
                  }
                }

                if (!force &amp;&amp; Windows.Count &gt; 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;
      }
</code></pre>
<p>当 ShutdownRequestedEventArgs 的 Cancel 被设置为 true 时,将传递到 libSM 的 SmcInteractDone 方法,从而通知会话管理器取消了本次操作。代码如下</p>
<pre><code class="language-csharp">                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);
</code></pre>
<p>如 https://www.x.org/releases/X11R7.7/doc/libSM/SMlib.html 文档所示,调用的 SmcInteractDone 方法的签名如下</p>
<pre><code>void SmcInteractDone(SmcConn smc_conn, Bool cancel_shutdown);
</code></pre>
<blockquote>
<p>After interacting with the user (in response to an “Interact” message), you should call SmcInteractDone</p>
<p>smc_conn:The session management connection object.</p>
<p>cancel_shutdown:If True, indicates that the user requests that the entire shutdown be cancelled.</p>
<p>cancel_shutdown:如果是 True 值,表示用户请求取消整个关闭操作</p>
</blockquote>
<p>这就意味着只要有窗口没有关闭,则会阻止系统关闭</p>
<p>在麒麟 ukui 底层里面,就依赖了 xsm 机制。当收到了有至少一个应用取消关闭时,将会取消关机等的令牌,从而取消关机等命令</p>
<p>由于 ukui 和 Windows 系统的机制不同,不会再弹出提示窗口说有哪些应用没有退出,是否强制关机。因此会导致用户产生困扰,用户将无法从开始菜单进行关机。我认为更好的解决方法是让 ukui 仿照 Windows 系统的做法,弹出提示窗口,如果用户没有主动取消关机,则等几秒强制关机</p>


</div>
<div id="MySignature" role="contentinfo">
    <p>博客园博客只做备份,博客发布就不再更新,如果想看最新博客,请访问 https://blog.lindexi.com/</p>

<p>如图片看不见,请在浏览器开启不安全http内容兼容</p>

<img alt="知识共享许可协议" style="border-width: 0" src="https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png"><br>本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名[林德熙](https://www.cnblogs.com/lindexi)(包含链接:https://www.cnblogs.com/lindexi ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我[联系](mailto:lindexi_gd@163.com)。<br><br>
来源:https://www.cnblogs.com/lindexi/p/18924837
頁: [1]
查看完整版本: Avalonia 取消窗口关闭导致 Linux 麒麟系统无法关机注销重启