Daemon 模式下的一个坑:window-system 变量从 nil 到有

我对启动时间不太在意,2011 年的老 Mac,需要 10 多秒(不过我这时间是实实在在的,没有半点偷渡),如果刚好遇到什么任务冲突,100 多秒也是有的,无所谓。

所以我的配置没有认真在 Daemon 模式下跑过,刚才试了一下,发现背景色不太对劲,很快找到了问题配置所在:

(defun edit-server-edit-frame-setup (frame)
  (unless window-system
    (with-selected-frame frame
      ;;...
      )))

(add-hook 'after-make-frame-functions #'edit-server-edit-frame-setup)

这个函数用途是,在终端模式下对主题&背景做些调整。常规方式启动是没有问题的,但 emacsclient 启动 GUI,函数也生效了,这是不应该的。于是加了两行 message:

(defun edit-server-edit-frame-setup (frame)
  (unless window-system
    (message ">>> edit-server-edit-frame-setup 1 %S" window-system)
    (with-selected-frame frame
      (message ">>> edit-server-edit-frame-setup 2 %S" window-system)
      ;;...
      )))

(add-hook 'after-make-frame-functions #'edit-server-edit-frame-setup)

果然问题出在 window-system

edit-server-edit-frame-setup 1 nil
edit-server-edit-frame-setup 2 ns

在 emacsclient 启动 GUI 的过程中,window-system 有一段时间其实是 nil,待 frame 创建完成之后,它又变成了 ns,所以这段代码的判断就出错了。改成一下写法解决问题:

(defun edit-server-edit-frame-setup (frame)
  (with-selected-frame frame
    (unless window-system
      ;;...
      )))

由于存在这么一个从 nil 到有的过程,我如果打算长期使用 Daemon,整个配置可能要重新检视一遍,比如,有些代码区分是终端和 GUI 版本的:

(if window-system
  (defun foo-setup ()
    ;; aaa
    )
  (defun foo-setup ()
    ;; bbb
    ))

常规启动,无论 GUI 还是终端,只有一套代码生效。如果 emacsclient 启动,则必须这么写:

(defun foo-setup ()
  (if window-system
      (progn
        ;; aaa
        )
    (progn
      ;; bbb
      )))

因为要应对 emacsclient 随时都可能启动 GUI 或终端的情况,所以两种实现都得有效。仅这个例子来看,每次执行 foo-setup 都需要判断一次 (if window-system ...),而不是原来的只在初始化判断一次。

更严重的不是多了几次判断,而是由于 emacsclient 启动过程中,frame 创建的时机跟常规方式不同,所以也导致配置当中有用到 window-system (或其他跟 frame 相关的变量 )的代码没有按照预期执行:该执行的没执行,不该执行的偏又执行。

5 个赞

我每次用 Daemon 开出来的 modeline 都是不全的 :joy:

emacsclient display 有关的问题我觉得最优雅的解决方案是 spacemacs 提供的:

1 个赞

我认为常规启动和emacsclient的判断次数没什么区别。常规启动一个frame会判断一次,emacsclient创建一个frame也会判断一次。如果你常规启动两个frame也要判断两次,emacsclient创建两个frame也是判断两次。

前面写的太啰嗦了,这里做些补充

  • 为什么 window-system 的变化会带来混乱?

    Daemon 启动(也就是整个配置文件加载)时,是被当作终端对待的,所以针对终端的各种配置/hook都已经加载了,针对 GUI 的则被忽略。当启动一个 GUI client 时,它执行的有一部分可能是针对终端的配置。

    .--------.
    | Daemon |
    '--------'
         |
         | window-system: nil
         |
         |     .-------------.
         |     | Client(GUI) |
         |     '-------------'
         |           |
         |---------->|
         |           |
         v           | window-system: nil
                     |\
                     | | create frame
                     |/
                     | window-system: ns
                     |
                     v
    
  • 为什么要在 (if window-system ...) 里面定义函数?有什么区别?

    感觉这样会快一些(想当然),区别嘛,用 c 语言对比更明了:

    ;;     elisp        ;;       c
    ;;--------------------------------------------
    (if window-system   ;;  void foo () {
        (defun foo ()   ;;  #ifdef WINDOW_SYSTEM
          BODY-AAA)     ;;      BODY_AAA
      (defun foo ()     ;;  #else
        BODY-BBB))      ;;      BODY_BBB 
                        ;;  #end
                        ;;  }
    ;;--------------------------------------------
    (defun foo ()       ;; void foo () {
      (if window-system ;;     if (WINDOW_SYSTEM) {
          BODY-AAA      ;;         BODY_AAA
        BODY-BBB))      ;;     } else {
                        ;;         BODY_BBB
                        ;;     }
                        ;; }
    

试试 (display-graphic-p),参考 Window Systems 最后一段。

这段代码要改写成advice-add要怎么做?

我试过

(defun cm//init-dispaly () ...)

(advice-add 'system-create-window-system-frame :after 'cm//init-display)

不过似乎并没有正确的运行