4

Something setqs desktop-dirname to nil after my "init.el" is loaded. I want to track down the offending code.

Just before the end of "init.el", desktop-dirname is correctly set.

I checked run-hooks by giving advise to it, but the variable was found to be set outside of run-hooks. But there are more hook-runners, timers, etc...

Is there a general technique to pin-point the code that writes to a variable?

mnish
  • 55
  • 4
  • Start by bisecting your init file, to see if something it is doing or loading changes this behavior (later on). Similarly, if you use a `custom-file`. And `grep` the code you load for `desktop-dirname`. And check `desktop.el` for its handling of that variable. There is no silver bullet to finding out what changed some global variable. – Drew Oct 20 '16 at 13:48
  • 4
    I have a [partially finished patch to Emacs that would let you invoke the debugger when a variable is modified](https://lists.nongnu.org/archive/html/emacs-devel/2015-11/msg02454.html), I plan to update it in a couple of weeks or so. – npostavs Oct 20 '16 at 17:58
  • 1
    @Drew Yeah, but that takes like O(log n) time, with not-so-cheap constant factor. Also, expressions can be dynamically generated, so static analysis like grepping is never enough. And in my case `grep` unfortunately revealed nothing... maybe I did something fancy in the past. – mnish Oct 20 '16 at 18:09
  • @npostavs Sounds great! Does your patch enable attaching arbitrary code to execute? I'm thinking of something like "attributed variables" in Prolog, which trigger code when variables were set. – mnish Oct 20 '16 at 18:17
  • @npostavs Maybe if you could explain your patch in an answer, I will accept it. Thanks to your notification. – mnish Oct 20 '16 at 18:20

2 Answers2

10

This is still under development, but in Emacs 26.1 (assuming nothing goes wrong) you will be able to do M-x debug-watch <variable> RET and then any change to the variable will land in the debugger with a backtrace.

You can get the patchset now, at https://debbugs.gnu.org/cgi/bugreport.cgi?bug=24923.

Does your patch enable attaching arbitrary code to execute?

Yes, the mechanism allows running any function when a symbol value is modified.

npostavs
  • 9,033
  • 1
  • 21
  • 53
2

There is a similar question there: Running Elisp code when a variable is changed that is closed as a duplicate. The feature is needed there for emacs 24.5.

You get the backtrace at a setq of a critical symbol with the following code. Execute the code below and (debug-setq 'desktop-dirname) before you load the critical section of your lisp code. Please note the remark of npostavs stating that no part of the critical section of your lisp code should be byte-compiled. Be warned that there may be auto-loads and requires in that section.

(defvar setq-critical-symbol)
(setq setq-critical-symbol nil)

(declare-function #'old-setq "subr" t t)

(unless (fboundp #'old-setq)
  (fset #'old-setq (symbol-function #'setq)))

(defun reset-setq ()
  "Mainly as a savior in the case of emergency."
  (interactive)
  (assert (fboundp #'old-setq)) ; should not happen with the form before the definition of `reset-setq'
  (old-setq setq-debug nil)
  (fset #'setq (symbol-function #'old-setq)))

(defun debug-setq (&optional critical-symbol)
  "Instruments `setq' such that `backtrace' is called if the `symbol-value' of a critical symbol is set via `setq'.
The critical symbol is specified by the variable `setq-critical-symbol'.
If CRITICAL-SYMBOL is non-nil `setq-critical-symbol' is set to that value (a symbol)."
  (interactive "SCritical symbol:")
  (setq setq-critical-symbol critical-symbol)
  (defmacro setq (&rest args)
    (let ((myargs (make-symbol "*myargs*")))
      `(let ((,myargs '(,@args))
             (ret (old-setq ,@args))
             debug-on-entry)
         (when setq-critical-symbol
           (while ,myargs
             (when (eq (car ,myargs) setq-critical-symbol)
               (fset #'setq (symbol-function #'old-setq)) ;; maybe that is over-cautious
               (old-setq setq-critical-symbol nil)
               (backtrace))
             (old-setq ,myargs (cddr ,myargs))))
         ret))))

A test demonstrating the function (can be run line-wise in the scratch-buffer):

(debug-setq 'test-arg)

(defun fun-with-critical-setq ()
  (setq test-arg 0))

(fun-with-critical-setq)

(setq setq-critical-symbol nil)

(fun-with-critical-setq)

The first call to fun-with-critical-setq spits the following backtrace out:

  backtrace()
  (progn (fset (function setq) (symbol-function (function old-setq))) (old-setq setq-critical-symbol nil) (backtrace))
  (if (eq (car *myargs*) setq-critical-symbol) (progn (fset (function setq) (symbol-function (function old-setq))) (old-setq setq-critical-symbol nil) (backtrace)))
  (while *myargs* (if (eq (car *myargs*) setq-critical-symbol) (progn (fset (function setq) (symbol-function (function old-setq))) (old-setq setq-critical-symbol nil) (backtrace))) (old-setq *myargs* (cdr (cdr *myargs*))))
  (progn (while *myargs* (if (eq (car *myargs*) setq-critical-symbol) (progn (fset (function setq) (symbol-function (function old-setq))) (old-setq setq-critical-symbol nil) (backtrace))) (old-setq *myargs* (cdr (cdr *myargs*)))))
  (if setq-critical-symbol (progn (while *myargs* (if (eq (car *myargs*) setq-critical-symbol) (progn (fset (function setq) (symbol-function (function old-setq))) (old-setq setq-critical-symbol nil) (backtrace))) (old-setq *myargs* (cdr (cdr *myargs*))))))
  (let ((*myargs* (quote (test-arg 0))) (ret (old-setq test-arg 0)) debug-on-entry) (if setq-critical-symbol (progn (while *myargs* (if (eq (car *myargs*) setq-critical-symbol) (progn (fset (function setq) (symbol-function ...)) (old-setq setq-critical-symbol nil) (backtrace))) (old-setq *myargs* (cdr (cdr *myargs*)))))) ret)
  fun-with-critical-setq()
  eval((fun-with-critical-setq) nil)
  elisp--eval-last-sexp(nil)
  eval-last-sexp(nil)
  funcall-interactively(eval-last-sexp nil)
  call-interactively(eval-last-sexp nil nil)
  command-execute(eval-last-sexp)
0 (#o0, #x0, ?\C-@)

You must ignore the overhead caused by the customized setq at the beginning of the backtrace. The second call to fun-with-critical-setq works like the original setq for two reasons:

  1. The customized setq resets its own symbol to the original version.
  2. The customized setq works like the original version if setq-critical-symbol is set to nil.

The second reason means that if you just call (debug-setq) instead of (debug-setq 'test-arg) the customized setq works like the original variant until you set setq-critical-symbol.

Tobias
  • 32,569
  • 1
  • 34
  • 75
  • That's a neat idea. You should additionally make sure to load only source files, because I think it wouldn't work for compiled code though (because the `setq` gets turned into a bytecode, so the indirection via `setq` symbol is lost). – npostavs Apr 28 '17 at 20:02
  • @npostavs Thanks for the comment. I added a remark about that to the answer. – Tobias Apr 28 '17 at 23:47