2

I'm having some difficulties when trying to set something up that saves some persistent state, so that I can use the data between emacs invocations.

Using as a starting point some code from another question, I came up with the following little code snippet for something I'm wanting to do:

(defmacro with-output-to-file (path &rest body)
  "record output of commands in body to file"
  `(save-excursion
     (let* ((buf (find-file-noselect ,path))
            (standard-output buf))
       (set-buffer buf)
       (erase-buffer)
       ,@body
       (save-buffer)
       (kill-buffer))))

I then have a function that uses this, like:

(defun my-save-some-data ()
  (with-output-to-file my-data-save-file
                       (prin1 my-data)))

EDIT: These both follow code like the following (previously, these were both setq; thanks to a comment from @phils for inspiring me to switch them to devfar and defcustom):

; note: my actual variable names (and filename value) are different;
; changed for example sake:
(defvar my-data (make-hash-table :test 'equal) "Data for a thing")
(defcustom my-data-save-file "~/tmp/my-data.el" "File to save my data to")

(Note: I also have a function to read the data back in, which happens automatically at load time, or on demand.)

I've set that up to run in a few circumstances (maybe too many? maybe poor choices? Anyway, this is what I set up):

(add-hook 'auto-save-hook 'my-save-some-data)
(add-hook 'kill-emacs-hook 'my-save-some-data)
(add-hook 'post-gc-hook 'my-save-some-data)

Most of the time, this works fine. However, every once in a while, I'm getting a problem where the data gets written to one of my previously-open buffers (killing all previous content there!), and then that buffer gets killed, with the saved changes.

Suffice it to say, this is highly annoying, as the buffer where this happens is frequently somewhere where I've been doing some work, and not necessarily checked it in yet.

I tried altering the macro above, replacing from (set-buffer buf) on with:

       (with-current-buffer buf ; because set-buffer wasn't working??
         (erase-buffer)
         ,@body
         (if (eq buf (current-buffer))
             (progn
               (save-buffer)
               (kill-buffer))
           (message "buffer changed?!"))))))

This has somehow managed to cause it to append to the buffer, instead of overwriting it... so my if statement does seem to be working to some degree... however I don't see the message in my *Messages* buffer, so... I'm not quite sure what's going on.

One thing I think I've noticed (though it's hard to be certain, since I may not be actively paying attention when this happens) is that this happens in a not-then-currently-active buffer, rather than a buffer I'm currently editing.

So, the questions:

  1. Am I doing something wrong here?
  2. Are there other/better ways of doing this?
  3. Are there standard ways to save state in a programatic way, that I could be using? (I poked around a bit in apropos, but failed to find anything... though perhaps I just don't know what to look for.)
  4. What can I do to help myself track this down? (is there a way I can set breakpoints or something?)
  5. Are there other protections I could use in code like this?

Any other thoughts welcome. I'm adding some more (message) forms in hopes of getting more debugging info in the mean time.

UPDATE: I've figured out that this only happens with the post-gc-hook. I don't know if my variables were somehow getting clobbered (and perhaps switching to defvar and defcustom will solve that?), or if there's some sort of obscure bug in the post-gc-hook processing... checking for reproducing the test-case with this latest change.

Community
  • 1
  • 1
lindes
  • 9,854
  • 3
  • 33
  • 45
  • are you sure that `,@body` doesn't change the current buffer? and are you sure that the buffer that gets overwritten against your wish is not the same as the one specified by `,path`? – ealfonso Oct 08 '13 at 23:58
  • 1
    I may as well ask the obvious -- is there any code which modifies `my-data-save-file` ? – phils Oct 09 '13 at 01:43
  • @phils: I have an initial `setq` to set it up, and then it gets used once as shown in `my-save-some-data`, and once more in the (not presented in my question) `read`-related function, which does a `(with-temp-buffer (insert-file-contents ...` on it, and then `read-from-string`. So, unless the variable is being accessed by some other code, no, I don't modify it. And anyway, it goes back to behaving correctly in later invocations (without having re-called the `setq`). – lindes Oct 09 '13 at 07:04
  • @erjoalgo: the contents of ,@body are just what's shown in my original question, namely: `(prin1 my-data)`. So, I don't think so? – lindes Oct 09 '13 at 07:06
  • lindes: You'll want to `defvar` (or `defcustom`) it, but this shouldn't be a factor unless you're using lexical binding for the file. (You should declare dynamic variables regardless, though.) The other possibility would be buffer-local values for the variable, but nothing you've mentioned suggests that would be the case (unless you were experimenting with it previously, and are still running that instance). – phils Oct 09 '13 at 08:17
  • So, I've discovered that this corruption only happens when I've run `(add-hook 'post-gc-hook 'my-save-some-data)`. Turning that off is fine for my needs, and may well solve my problem... However, I still don't understand why being called in the GC case should cause my to use (and corrupt) a different buffer. I'm switching the two `setq`s to a `defvar` and a `defcustom` (thanks for that pointer, @phils), respectively, and I'll see if that changes anything... – lindes Oct 14 '13 at 19:14
  • That does sound a bit disconcerting. Make sure you `M-x report-emacs-bug` if you conclude that it must be one. – phils Oct 14 '13 at 21:11
  • @phils: thanks, will do... though I'm shy of "conclusions" point as yet. It does sort of seem like switching to `defvar` and `defcustom` may have stopped it from happening. That, or I just haven't hit the latest instance yet. Though narrowing it down, I'm wondering if, reverting to past behavior, I can find a reproducible test case, and trigger it with `M-x garbage-collect`. I'll try to do that. – lindes Oct 15 '13 at 03:14

2 Answers2

2

You can indeed set breakpoints, an easy way to do this is to put (edebug) in the place where you want to break. Then you can use, n for next, SPC for step, and e to eval. You can read more about edebug here.

So you can set a conditional breakpoint as a protection/warning, like this, before your call to (set-buffer):

(when (get-file-buffer my-data-save-file) 
 (read-from-minibuffer 
   (format "Warning: %s is already being visited by a buffer, contents will be overwritten! Entering edebug" my-data-save-file))
   (edebug))

This will warn you and then enter the debugger if a file you are visiting in some buffer is about to be overwritten by your macro, where you can inspect what is going on.

Here is part the docstring of find-file-no-select:

Read file FILENAME into a buffer and return the buffer.
If a buffer exists visiting FILENAME, return that one, but
verify that the file has not changed since visited or saved.

My guess is that the my-data-save-file is already being visited by a buffer, so that is the buffer that is returned (and subsequently overwritten). But you can really find out what is happening with (edebug).

ealfonso
  • 6,622
  • 5
  • 39
  • 67
  • Hmm, thank you for the `edebug` and other pointers... I'll poke at this, for sure. However, I will say that I'm about 99.9% sure that I haven't had a buffer open for `my-data-save-file`. Also, even if I have, I'm 100% certain that that's not what got clobbered. What got clobbered was, for example, a `.c` file that I was editing, one time, and a different `.lisp` file a different time... neither of which close in directory structure to the value of `my-data-save-file`. – lindes Oct 09 '13 at 06:55
  • Well, what gets clobbered can not be anything but `buf` which is set by (`find-file-no-select...)`, presumably on the file `my-data-save-file`. So maybe you should check the contents of `my-data-save-file` before anything is overwritten, something like `(read-from minibuffer (format "Confirm to overwrite this file: %s" my-data-save-file))`. If `set-buffer` does not succeed, I believe an error is signaled, so the function would not proceed from that point. Either way, try adding those snippets and I'm sure soon you'll figure it out. – ealfonso Oct 09 '13 at 07:25
  • My goal is to have this be quite non-interactive, in general... I could certainly try setting up a minibuffer something, as a debugging tool, but I don't want to have to confirm that on a regular basis... Also, it turns out that `edebug` isn't available in the form you list? I get `Symbol's function definition is void: edebug` ... is that from a different version of emacs? I'm using `GNU Emacs 24.3.1 (x86_64-apple-darwin10.8.0, NS apple-appkit-1038.36)`, from MacPorts port `emacs-app @24.3_1`. There are some other `edebug`-prefixed functions, but none of them seem to be quite what you mean? – lindes Oct 09 '13 at 18:22
  • 1
    Yes, I suggested the minibuffer warning just as temporary solution in order to catch the undesired behavior when it happens and to be able to inspect what is going on at that moment with `(edebug)`, but once you fix it you should turn it off. That's strange, I'm using 23.3, but I downloaded the source code for your version and it does show `edebug.el` which `provide`s edebug. Have you tried `(require 'edebug)`? – ealfonso Oct 09 '13 at 19:08
  • I just had to install 24.3 for an unrelated reason, but I have no problems with edebug. – ealfonso Oct 10 '13 at 04:02
1

Just a quick reply to some of what you said. Your message never appears probably because you test whether the buffer of with-current-buffer is the current-buffer, which it always is, unless body changes the current buffer.

But you are right to use with-current-buffer instead of save-excursion followed by set-buffer.

As for other ways: why not put your data in a temporary buffer and then use write-file or append-to-fileor write-region?

FWIW, I tried your code briefly and saw no problem. But I just tried a simple (prin1 (symbol-function 'my-save-some-data)) for the body and a constant file name for the file. I tried with pre-existing file or not, and with pre-existing buffer or not, and with pre-existing unsaved modified buffer or not.

Are you testing with the interpreted code (e.g., macro present) or byte-compiled code?

Drew
  • 29,895
  • 7
  • 74
  • 104
  • I suppose I should mention that the content of `my-data` is a hash, originally created with `(make-hash-table :test 'equal)` (and later persisted via `(read ...)`). As for `write-file`, I could certainly try doing that... Would that be safer, since I always supply a filename? (I.e. perhaps I'd get corruption the other way around, with the content of some other buffer landing in my `my-data-save-file`, but at least the other buffer not getting killed? Then again, I still am programmatically filling a buffer, so if it switches on me, it still might create grief... I'll look at the others, too – lindes Oct 09 '13 at 06:59
  • Oh, and I haven't done any byte compiling yet... just using C-x C-e to eval the `defmacro` and the `defun`. Well, I added a `load` on the whole file, too, once I thought I had things working, that's in `.emacs.d/init.el`... but that just loads it, right? There is no .elc file for the file being loaded. – lindes Oct 09 '13 at 07:10
  • OK. Yes, it's easier to deal with just the interpreted code, at first. And loading the `*.el` just, well, loads the `*.el` (interpreted, not compiled). Can't answer your question about whether `write-file` would be safer. – Drew Oct 09 '13 at 16:03
  • Are there any functions to which I could just supply a filename and a content string, and have it dump that content to that file, without ever opening a buffer? at a *conceptual* level, that's what (I think) I want. – lindes Oct 09 '13 at 18:23
  • 1
    `write-region` and `append-to-file` do that, but the string has to be the region. But it's not a big deal to just use `(with-current-buffer (find-file-noselect FILE)...(insert STRING) (write-file FILE))`. Very easy. – Drew Oct 09 '13 at 18:37
  • Thanks, @Drew... I imagine I might try re-doing this with something closer to this approach. You're leaving out the `standard-out` override, yet I see that `prin1` can take a buffer, so maybe I can restructure this in a way that either makes the macro anaphoric (can I do that in elisp? I'll find out!) or skips the macro entirely, and just approach the problem differently. Anyway, we'll see if that actually solves the corruption. I'm starting to suspect something gets swapped out during GC, though, when I use that version? More debugging to come. – lindes Oct 10 '13 at 23:41
  • Not sure exactly what you are doing, but in what I wrote you just insert the string using `insert` --- you don't need `prin1` (or binding `standard-output`). You are not printing to standard output; you are inserting text in a buffer: `insert`. – Drew Oct 11 '13 at 02:17
  • I'm looking for the "printed representation" of an object (which `prin1` gives); the value is not a string. `insert` asks for "either strings or characters". I suppose I could use `prin1-to-string`... Might my `standard-output` be getting changed out from under me, mid-function? If so, that might be my solution. (edit: I would then presumably still use `insert` with that as its argument.) – lindes Oct 14 '13 at 19:28
  • 1
    Without seeing more of the code I cannot really suggest much. Yes, you can use `prin1-to-string` (or `(format "%S" THING)`). – Drew Oct 14 '13 at 20:19
  • I believe I've presented all the code that's really relevant. There is other code that modifies the content of `my-data`, using `gethash` and `put-hash`. And it's true I've modified my code slightly before copy/pasting. I can try to repro it with the code I actually present in my question, if that's helpful, though. – lindes Oct 15 '13 at 03:29