unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
@ 2021-02-09  9:47 Peter
  2021-02-09 23:47 ` Matt Armstrong
  0 siblings, 1 reply; 55+ messages in thread
From: Peter @ 2021-02-09  9:47 UTC (permalink / raw)
  To: 46397

The following steps reproduce this:
- Make sure /tmp/tmp does not exist
- Open a buffer /tmp/tmp/foo.txt
- Make some changes to the buffer.
- Do *not* save the buffer or create the directory /tmp/tmp/
- Create /tmp/tmp as a *file* (not a directory)
- Try to kill the buffer.

You will see the following error message:

Unlocking file: Not a directory, /tmp/tmp/foo.txt

I just want to kill the buffer, I don't want to save it.

This is easily rectified by just deleting / moving away /tmp/tmp, but it
seems like this could be resolved to just delete the buffer.

Greetings, Peter



In GNU Emacs 27.1 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.22, cairo version 1.17.3)
 of 2020-08-28 built on juergen
Windowing system distributor 'The X.Org Foundation', version 11.0.12010000
System Description: Arch Linux

Recent messages:
<right-margin> <triple-down-mouse-4> is undefined
<right-margin> <triple-mouse-4> is undefined
<right-margin> <triple-down-mouse-4> is undefined
<right-margin> <triple-mouse-4> is undefined
Mark saved where search started
Mark set
Mark saved where search started [2 times]
mouse-2: copy to input; mouse-3: menu [3 times]
Mark set [4 times]
Mark saved where search started [3 times]

Configured using:
 'configure --prefix=/usr --sysconfdir=/etc --libexecdir=/usr/lib
 --localstatedir=/var --with-x-toolkit=gtk3 --with-xft --with-wide-int
 --with-modules --with-cairo --with-harfbuzz 'CFLAGS=-march=x86-64
 -mtune=generic -O2 -pipe -fno-plt' CPPFLAGS=-D_FORTIFY_SOURCE=2
 LDFLAGS=-Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now'

Configured features:
XPM JPEG TIFF GIF PNG RSVG CAIRO SOUND GPM DBUS GSETTINGS GLIB NOTIFY
INOTIFY ACL GNUTLS LIBXML2 FREETYPE HARFBUZZ M17N_FLT LIBOTF ZLIB
TOOLKIT_SCROLL_BARS GTK3 X11 XDBE XIM MODULES THREADS LIBSYSTEMD JSON
PDUMPER LCMS2 GMP

Important settings:
  value of $LC_COLLATE: C
  value of $LC_MESSAGES:
  value of $LANG: en_GB.UTF-8
  locale-coding-system: utf-8-unix





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-09  9:47 bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file Peter
@ 2021-02-09 23:47 ` Matt Armstrong
  2021-02-10  0:23   ` Matt Armstrong
  2021-02-10  0:26   ` Matt Armstrong
  0 siblings, 2 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-09 23:47 UTC (permalink / raw)
  To: Peter; +Cc: 46397

"Peter" <craven@gmx.net> writes:

> The following steps reproduce this:
> - Make sure /tmp/tmp does not exist
> - Open a buffer /tmp/tmp/foo.txt
> - Make some changes to the buffer.
> - Do *not* save the buffer or create the directory /tmp/tmp/
> - Create /tmp/tmp as a *file* (not a directory)
> - Try to kill the buffer.
>
> You will see the following error message:
>
> Unlocking file: Not a directory, /tmp/tmp/foo.txt
>
> I just want to kill the buffer, I don't want to save it.
>
> This is easily rectified by just deleting / moving away /tmp/tmp, but it
> seems like this could be resolved to just delete the buffer.

I confirmed this in 27.1 and the master branch. I also found it
difficult to exit Emacs once this state was reached. If you choose "no"
when saving modified buffers Emacs hits this error and is left in a
strange state (there are no displayed buffers or mode line, but Emacs is
still running).

The backtrace is unsurprising:

Debugger entered--Lisp error: (file-error "Unlocking file" "Not a directory" "/private/tmp/tmp/test.txt")
  kill-buffer("test.txt")
  funcall-interactively(kill-buffer "test.txt")
  call-interactively(kill-buffer nil nil)
  command-execute(kill-buffer)





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-09 23:47 ` Matt Armstrong
@ 2021-02-10  0:23   ` Matt Armstrong
  2021-02-10 15:05     ` Eli Zaretskii
  2021-02-10  0:26   ` Matt Armstrong
  1 sibling, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-02-10  0:23 UTC (permalink / raw)
  To: Peter; +Cc: 46397, Paul Eggert

[-- Attachment #1: Type: text/plain, Size: 1363 bytes --]

Matt Armstrong <matt@rfc20.org> writes:

> "Peter" <craven@gmx.net> writes:
>
>> The following steps reproduce this:
>> - Make sure /tmp/tmp does not exist
>> - Open a buffer /tmp/tmp/foo.txt
>> - Make some changes to the buffer.
>> - Do *not* save the buffer or create the directory /tmp/tmp/
>> - Create /tmp/tmp as a *file* (not a directory)
>> - Try to kill the buffer.
>>
>> You will see the following error message:
>>
>> Unlocking file: Not a directory, /tmp/tmp/foo.txt
>>
>> I just want to kill the buffer, I don't want to save it.

[...]

> The backtrace is unsurprising:
>
> Debugger entered--Lisp error: (file-error "Unlocking file" "Not a directory" "/private/tmp/tmp/test.txt")
>   kill-buffer("test.txt")
>   funcall-interactively(kill-buffer "test.txt")
>   call-interactively(kill-buffer nil nil)
>   command-execute(kill-buffer)

I found that this behavior was introduced by Paul Egger's commit
9dc306b1db0, discussed in Bug#37389.  I've cc'd Paul.

Paul's commit changed unlock_file() (from src/filelock.cc) to report
errors from unlink(), excempting only ENOENT. This bug demonstrates a
way to induce an ENOTDIR error. I've attached a patch that ignores
ENOTDIR as well, which is the most conservative fix I can think of. It
also seems in-line with Paul's original intent, since he was saying that
both ENOENT and ENOTDIR are usually "tame."


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Ignore ENOTDIR --]
[-- Type: text/x-patch, Size: 853 bytes --]

From fc0b7f2595bd8680952f062d2dd5261f94394e1c Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Tue, 9 Feb 2021 16:14:28 -0800
Subject: [PATCH] Ignore ENOTDIR errors from unlink().

* src/filelock.c (unlock_file): Ignore ENOTDIR errors from unlink().
---
 src/filelock.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/filelock.c b/src/filelock.c
index 35baa0c666..af5683f365 100644
--- a/src/filelock.c
+++ b/src/filelock.c
@@ -731,7 +731,8 @@ unlock_file (Lisp_Object fn)
   MAKE_LOCK_NAME (lfname, fn);
 
   int err = current_lock_owner (0, lfname);
-  if (err == -2 && unlink (lfname) != 0 && errno != ENOENT)
+  if (err == -2 && unlink (lfname) != 0
+      && (errno != ENOENT && errno != ENOTDIR))
     err = errno;
   if (0 < err)
     report_file_errno ("Unlocking file", filename, err);
-- 
2.30.0


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-09 23:47 ` Matt Armstrong
  2021-02-10  0:23   ` Matt Armstrong
@ 2021-02-10  0:26   ` Matt Armstrong
  1 sibling, 0 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-10  0:26 UTC (permalink / raw)
  To: Peter; +Cc: 46397, Paul Eggert

[-- Attachment #1: Type: text/plain, Size: 1363 bytes --]

Matt Armstrong <matt@rfc20.org> writes:

> "Peter" <craven@gmx.net> writes:
>
>> The following steps reproduce this:
>> - Make sure /tmp/tmp does not exist
>> - Open a buffer /tmp/tmp/foo.txt
>> - Make some changes to the buffer.
>> - Do *not* save the buffer or create the directory /tmp/tmp/
>> - Create /tmp/tmp as a *file* (not a directory)
>> - Try to kill the buffer.
>>
>> You will see the following error message:
>>
>> Unlocking file: Not a directory, /tmp/tmp/foo.txt
>>
>> I just want to kill the buffer, I don't want to save it.

[...]

> The backtrace is unsurprising:
>
> Debugger entered--Lisp error: (file-error "Unlocking file" "Not a directory" "/private/tmp/tmp/test.txt")
>   kill-buffer("test.txt")
>   funcall-interactively(kill-buffer "test.txt")
>   call-interactively(kill-buffer nil nil)
>   command-execute(kill-buffer)

I found that this behavior was introduced by Paul Egger's commit
9dc306b1db0, discussed in Bug#37389.  I've cc'd Paul.

Paul's commit changed unlock_file() (from src/filelock.cc) to report
errors from unlink(), excempting only ENOENT. This bug demonstrates a
way to induce an ENOTDIR error. I've attached a patch that ignores
ENOTDIR as well, which is the most conservative fix I can think of. It
also seems in-line with Paul's original intent, since he was saying that
both ENOENT and ENOTDIR are usually "tame."


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Ignore ENOTDIR --]
[-- Type: text/x-patch, Size: 853 bytes --]

From fc0b7f2595bd8680952f062d2dd5261f94394e1c Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Tue, 9 Feb 2021 16:14:28 -0800
Subject: [PATCH] Ignore ENOTDIR errors from unlink().

* src/filelock.c (unlock_file): Ignore ENOTDIR errors from unlink().
---
 src/filelock.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/filelock.c b/src/filelock.c
index 35baa0c666..af5683f365 100644
--- a/src/filelock.c
+++ b/src/filelock.c
@@ -731,7 +731,8 @@ unlock_file (Lisp_Object fn)
   MAKE_LOCK_NAME (lfname, fn);
 
   int err = current_lock_owner (0, lfname);
-  if (err == -2 && unlink (lfname) != 0 && errno != ENOENT)
+  if (err == -2 && unlink (lfname) != 0
+      && (errno != ENOENT && errno != ENOTDIR))
     err = errno;
   if (0 < err)
     report_file_errno ("Unlocking file", filename, err);
-- 
2.30.0


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-10  0:23   ` Matt Armstrong
@ 2021-02-10 15:05     ` Eli Zaretskii
  2021-02-10 19:23       ` Paul Eggert
  0 siblings, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-10 15:05 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <gmatta@gmail.com>
> Date: Tue, 09 Feb 2021 16:23:09 -0800
> Cc: 46397@debbugs.gnu.org, Paul Eggert <eggert@cs.ucla.edu>
> 
> > The backtrace is unsurprising:
> >
> > Debugger entered--Lisp error: (file-error "Unlocking file" "Not a directory" "/private/tmp/tmp/test.txt")
> >   kill-buffer("test.txt")
> >   funcall-interactively(kill-buffer "test.txt")
> >   call-interactively(kill-buffer nil nil)
> >   command-execute(kill-buffer)
> 
> I found that this behavior was introduced by Paul Egger's commit
> 9dc306b1db0, discussed in Bug#37389.  I've cc'd Paul.
> 
> Paul's commit changed unlock_file() (from src/filelock.cc) to report
> errors from unlink(), excempting only ENOENT. This bug demonstrates a
> way to induce an ENOTDIR error. I've attached a patch that ignores
> ENOTDIR as well, which is the most conservative fix I can think of. It
> also seems in-line with Paul's original intent, since he was saying that
> both ENOENT and ENOTDIR are usually "tame."

I think instead of ignoring some errors, we should allow the user to
get out of these situations, after showing the error.  That is,
instead of silently ignoring the error on some low level, we should
propagate it to userlock.el and allow the user to decide whether
he/she wants to ignore the error or do something about that.  These
errors could mean something innocent, or they could mean something
more serious, and we shouldn't second-guess which one is it.

Letting the user decide will also seamlessly solve any other type of
error that could happen when we try to delete the lock file.

So how about coming up with a patch that shows the error message to
the users and asks them whether or not to ignore that and kill the
buffer?

Thanks.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-10 15:05     ` Eli Zaretskii
@ 2021-02-10 19:23       ` Paul Eggert
  2021-02-10 19:45         ` Eli Zaretskii
  2021-02-11 22:14         ` Matt Armstrong
  0 siblings, 2 replies; 55+ messages in thread
From: Paul Eggert @ 2021-02-10 19:23 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: Matt Armstrong, 46397, craven

[-- Attachment #1: Type: text/plain, Size: 2493 bytes --]

On 2/10/21 7:05 AM, Eli Zaretskii wrote:

> I think instead of ignoring some errors, we should allow the user to
> get out of these situations, after showing the error.  That is,
> instead of silently ignoring the error on some low level, we should
> propagate it to userlock.el and allow the user to decide whether
> he/she wants to ignore the error or do something about that.  These
> errors could mean something innocent, or they could mean something
> more serious, and we shouldn't second-guess which one is it.

I agree with this. However, I think there are two bugs here.

The first bug is that the low-level locking code is busted, in that it 
thinks there's a lock file when ENOTDIR says there isn't. I installed 
the first attached patch into master to fix this bug. This patch isn't 
what Matt suggested - it's a bit earlier in the low-level C code where 
the bug really occurred. (The area that Matt was looking at was later 
on, when we have the lock and are removing it, and the ENOENT check 
there is for the rare case where NFS is being used and its unlink 
implementation gets confused and fails with ENOENT even though it was 
actually successful and where any error other than ENOENT is serious.)

The second bug is that once you're in a tricky situation where the file 
can't be unlocked for whatever reason and you attempt to exit Emacs, 
Emacs tries to auto-save the buffer, fails because of the lock problem, 
and then gets into a weird state where you cannot do anything. This 
problem can happen in other scenarios. For example:

* Run Emacs and visit the file /tmp/a/b where /tmp/a does not exist. 
Emacs will warn "Use M-x make-directory RET RET to create the directory 
and its parents"; ignore the warning.

* Type some characters so that /tmp/a/b's buffer is nonempty.

* Create an inaccessible directory /tmp/a by running "mkdir -m 0 /tmp/a" 
outside Emacs.

* Type C-x C-c to exit Emacs. It will say "Save file /tmp/a/b?" Type 
"n". It will then say "Modified buffers exist; exit anyway? (yes or 
no)". Type "yes". Emacs will then hang, in a weird state where it is 
trying to auto-save but hanging in the middle of that.

I did not fix this latter problem, so it needs further investigation. 
While looking into it, though, I did see some longstanding code in 
files.el that could use a bit of sprucing up given the more-modern 
primitives available now, and so installed the second attached patch 
into master.

[-- Attachment #2: 0001-Fix-file-lock-issue-Bug-46397.patch --]
[-- Type: text/x-patch, Size: 958 bytes --]

From 4459dcc07865f6ae1f21f624fcb09cf8fdaecdb5 Mon Sep 17 00:00:00 2001
From: Paul Eggert <eggert@cs.ucla.edu>
Date: Wed, 10 Feb 2021 10:50:44 -0800
Subject: [PATCH 1/2] Fix file lock issue (Bug#46397)

* src/filelock.c (current_lock_owner):
Also treat ENOTDIR as meaning the lock file does not exist.
---
 src/filelock.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/filelock.c b/src/filelock.c
index 35baa0c666..373fc00a42 100644
--- a/src/filelock.c
+++ b/src/filelock.c
@@ -532,7 +532,7 @@ current_lock_owner (lock_info_type *owner, char *lfname)
   /* If nonexistent lock file, all is well; otherwise, got strange error. */
   lfinfolen = read_lock_data (lfname, owner->user);
   if (lfinfolen < 0)
-    return errno == ENOENT ? 0 : errno;
+    return errno == ENOENT || errno == ENOTDIR ? 0 : errno;
   if (MAX_LFINFO < lfinfolen)
     return ENAMETOOLONG;
   owner->user[lfinfolen] = 0;
-- 
2.27.0


[-- Attachment #3: 0002-Simplify-and-speed-up-after-find-file.patch --]
[-- Type: text/x-patch, Size: 2349 bytes --]

From 4467073c50d2c7fbbb30530d1a0a25f8272ff56f Mon Sep 17 00:00:00 2001
From: Paul Eggert <eggert@cs.ucla.edu>
Date: Wed, 10 Feb 2021 10:55:42 -0800
Subject: [PATCH 2/2] Simplify and speed up after-find-file

Use newer primitives like file-accessible-directory-p to simplify
and speed up longstanding code in after-find-file.
* lisp/files.el (after-find-file):
Prefer file-exists-p + file-symlink-p to file-attributes +
file-symlink-p + file-chase-links + file-exists-p.
Prefer file-accessible-directory-p to directory-file-name +
file-attributes.
Prefer file-directory-p to file-name-directory + file-exists-p.
---
 lisp/files.el | 17 +++++++----------
 1 file changed, 7 insertions(+), 10 deletions(-)

diff --git a/lisp/files.el b/lisp/files.el
index dada69c145..9ff8f31e37 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -2530,13 +2530,11 @@ after-find-file
 	   (msg
 	    (cond
 	     ((not warn) nil)
-	     ((and error (file-attributes buffer-file-name))
+	     ((and error (file-exists-p buffer-file-name))
 	      (setq buffer-read-only t)
-	      (if (and (file-symlink-p buffer-file-name)
-		       (not (file-exists-p
-			     (file-chase-links buffer-file-name))))
-		  "Symbolic link that points to nonexistent file"
-		"File exists, but cannot be read"))
+	      "File exists, but cannot be read")
+	     ((and error (file-symlink-p buffer-file-name))
+	      "Symbolic link that points to nonexistent file")
 	     ((not buffer-read-only)
 	      (if (and warn
 		       ;; No need to warn if buffer is auto-saved
@@ -2553,13 +2551,12 @@ after-find-file
 	     ((not error)
 	      (setq not-serious t)
 	      "Note: file is write protected")
-	     ((file-attributes (directory-file-name default-directory))
+	     ((file-accessible-directory-p default-directory)
 	      "File not found and directory write-protected")
-	     ((file-exists-p (file-name-directory buffer-file-name))
-	      (setq buffer-read-only nil))
 	     (t
 	      (setq buffer-read-only nil)
-	      "Use M-x make-directory RET RET to create the directory and its parents"))))
+	      (unless (file-directory-p default-directory)
+		"Use M-x make-directory RET RET to create the directory and its parents")))))
       (when msg
 	(message "%s" msg)
 	(or not-serious (sit-for 1 t))))
-- 
2.27.0


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-10 19:23       ` Paul Eggert
@ 2021-02-10 19:45         ` Eli Zaretskii
  2021-02-10 22:39           ` Matt Armstrong
  2021-02-11 22:14         ` Matt Armstrong
  1 sibling, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-10 19:45 UTC (permalink / raw)
  To: Paul Eggert; +Cc: gmatta, 46397, craven

> Cc: craven@gmx.net, 46397@debbugs.gnu.org, Matt Armstrong <gmatta@gmail.com>
> From: Paul Eggert <eggert@cs.ucla.edu>
> Date: Wed, 10 Feb 2021 11:23:46 -0800
> 
> --- a/src/filelock.c
> +++ b/src/filelock.c
> @@ -532,7 +532,7 @@ current_lock_owner (lock_info_type *owner, char *lfname)
>    /* If nonexistent lock file, all is well; otherwise, got strange error. */
>    lfinfolen = read_lock_data (lfname, owner->user);
>    if (lfinfolen < 0)
> -    return errno == ENOENT ? 0 : errno;
> +    return errno == ENOENT || errno == ENOTDIR ? 0 : errno;

As I said, I don't think this is TRT.  Why should we silently ignore
ENOTDIR? couldn't it mean some kind of malicious attack on the user's
account, or some other trouble?

We should not ignore these errors, we should ask the user what to do
about them.  The user can tell us the error can be ignored, but we
should not decide that without asking.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-10 19:45         ` Eli Zaretskii
@ 2021-02-10 22:39           ` Matt Armstrong
  2021-02-12  7:43             ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-02-10 22:39 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: gmatta, 46397, Paul Eggert, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> Cc: craven@gmx.net, 46397@debbugs.gnu.org, Matt Armstrong <gmatta@gmail.com>
>> From: Paul Eggert <eggert@cs.ucla.edu>
>> Date: Wed, 10 Feb 2021 11:23:46 -0800
>> 
>> --- a/src/filelock.c
>> +++ b/src/filelock.c
>> @@ -532,7 +532,7 @@ current_lock_owner (lock_info_type *owner, char *lfname)
>>    /* If nonexistent lock file, all is well; otherwise, got strange error. */
>>    lfinfolen = read_lock_data (lfname, owner->user);
>>    if (lfinfolen < 0)
>> -    return errno == ENOENT ? 0 : errno;
>> +    return errno == ENOENT || errno == ENOTDIR ? 0 : errno;
>
> As I said, I don't think this is TRT.  Why should we silently ignore
> ENOTDIR? couldn't it mean some kind of malicious attack on the user's
> account, or some other trouble?
>
> We should not ignore these errors, we should ask the user what to do
> about them.  The user can tell us the error can be ignored, but we
> should not decide that without asking.

I think Paul's commit is a good one. I'll try to explain why.

The commit does not silently ignore ENOTDIR. Instead, it is explicitly
handles that particular error code it in a way that honors the lock file
API contract.

In this case, Paul's commit changes the current_lock_owner() function
such that it returns zero upon ENOTDIR. The caller must interpret the
zero return as meaning "at the time current_lock_owner() was called,
nobody owned the lock file...or the lock file was obsolete."

ENOTDIR has a specific meaning that we can rely on. Both ENOENT and
ENOTDIR imply that the file was definitely not on disk at the time of
the call. Because of this, current_lock_owner() can safely conclude that
nobody owned the lock.

Put another way, I think a clearer API spec for current_lock_owner()
would be:

/* Return -2 if the current process owns the lock file,

   or -1 if another process appears to own it (and set OWNER (if
       non-null) to info),

   or 0 if the lock file isn't there, or it is there but has no current
       owner,

   or an errno value if something is wrong with the locking mechanism. */

That spec is semantically equivalent to the current one, but makes it
more clear why ENOENT and ENOTDIR should cause the function to return
zero. Those errno values do not imply that "something is wrong with the
locking mechanism."

Up a level of abstraction, the callers of the lock file API are still
presented with coherent semantics, even if current_lock_owner() returns
"lock file not owned" when the directory is missing. If the higher level
code wants to acquire the lock, it will attempt that and get an error at
that time. If the higher level code wants to abandon a buffer, it can
just do that knowing that this process definitely doesn't own that lock.
In neither case the user need not be interrogated, and if they do, it
will be done at a time that makes much more sense to them -- when
actually trying to acquire the lock.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-10 19:23       ` Paul Eggert
  2021-02-10 19:45         ` Eli Zaretskii
@ 2021-02-11 22:14         ` Matt Armstrong
  2021-02-12  2:20           ` Paul Eggert
  1 sibling, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-02-11 22:14 UTC (permalink / raw)
  To: Paul Eggert, Eli Zaretskii; +Cc: 46397, craven

Paul Eggert <eggert@cs.ucla.edu> writes:

> The second bug is that once you're in a tricky situation where the file 
> can't be unlocked for whatever reason and you attempt to exit Emacs, 
> Emacs tries to auto-save the buffer, fails because of the lock problem, 
> and then gets into a weird state where you cannot do anything. This 
> problem can happen in other scenarios. For example:
>
> * Run Emacs and visit the file /tmp/a/b where /tmp/a does not exist. 
> Emacs will warn "Use M-x make-directory RET RET to create the directory 
> and its parents"; ignore the warning.
>
> * Type some characters so that /tmp/a/b's buffer is nonempty.
>
> * Create an inaccessible directory /tmp/a by running "mkdir -m 0 /tmp/a" 
> outside Emacs.
>
> * Type C-x C-c to exit Emacs. It will say "Save file /tmp/a/b?" Type 
> "n". It will then say "Modified buffers exist; exit anyway? (yes or 
> no)". Type "yes". Emacs will then hang, in a weird state where it is 
> trying to auto-save but hanging in the middle of that.
>
> I did not fix this latter problem, so it needs further investigation. 

Paul, I looked into this second issue a bit.

The issue isn't confined to exiting Emacs. It appears that once in a
"tricky situation where the file can't be unlocked for whatever reason"
Emacs will refuse to kill the buffer because unlock_file() signals an
error.

The problem is more severe when exiting Emacs because it occurs after
Emacs is in a half shut down state. The shutdown sequence does not take
kindly to the unhandled non-local exit.

I used this for a quick repro:

(cl-flet ((command-in-tmp (prog &rest args)
                   (with-temp-buffer
                     (cd-absolute "/tmp")
                     (apply #'call-process prog nil nil nil args))))
  (command-in-tmp "chmod" "u+rwx" "a")
  (command-in-tmp "rm" "-rf" "a")
  (find-file "/tmp/a/b")
  (insert "Hello, World!")
  (command-in-tmp "mkdir" "-m" "0" "a"))

At shutdown, the following call stack signals an error:

kill_emacs
 -> shut_down_emacs
  -> unlock_all_files
   -> unlock_file
    -> report_file_errno

In this case, the errno is "permission denied."

Note that shut_down_emacs() calls Fdo_auto_save() just before
unlock_all_files() and that call succeeds. Fdo_auto_save() also calls
report_file_errno, throwing an errno 13 (Permission denied), but that
recovers and continues.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-11 22:14         ` Matt Armstrong
@ 2021-02-12  2:20           ` Paul Eggert
  2021-02-12  7:15             ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Paul Eggert @ 2021-02-12  2:20 UTC (permalink / raw)
  To: Matt Armstrong, Eli Zaretskii; +Cc: 46397, craven

On 2/11/21 2:14 PM, Matt Armstrong wrote:

> The issue isn't confined to exiting Emacs. It appears that once in a
> "tricky situation where the file can't be unlocked for whatever reason"
> Emacs will refuse to kill the buffer because unlock_file() signals an
> error.

kill-buffer already asks the user something like "Buffer XXX modified; 
kill anyway? (yes or no)" when killing a buffer that's been changed 
without being saved. Perhaps it should also ask "File XXX cannot be 
unlocked; kill buffer anyway? (yes or no)" if the file can't be unlocked.

> Note that shut_down_emacs() calls Fdo_auto_save() just before
> unlock_all_files() and that call succeeds. Fdo_auto_save() also calls
> report_file_errno, throwing an errno 13 (Permission denied), but that
> recovers and continues.

Presumably shut_down_emacs should recover and continue if 
unlock_all_files fails when it directly calls unlock_all_files.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-12  2:20           ` Paul Eggert
@ 2021-02-12  7:15             ` Eli Zaretskii
  2021-02-13  1:15               ` Matt Armstrong
  0 siblings, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-12  7:15 UTC (permalink / raw)
  To: Paul Eggert; +Cc: gmatta, 46397, craven

> Cc: 46397@debbugs.gnu.org, craven@gmx.net
> From: Paul Eggert <eggert@cs.ucla.edu>
> Date: Thu, 11 Feb 2021 18:20:44 -0800
> 
> On 2/11/21 2:14 PM, Matt Armstrong wrote:
> 
> > The issue isn't confined to exiting Emacs. It appears that once in a
> > "tricky situation where the file can't be unlocked for whatever reason"
> > Emacs will refuse to kill the buffer because unlock_file() signals an
> > error.
> 
> kill-buffer already asks the user something like "Buffer XXX modified; 
> kill anyway? (yes or no)" when killing a buffer that's been changed 
> without being saved. Perhaps it should also ask "File XXX cannot be 
> unlocked; kill buffer anyway? (yes or no)" if the file can't be unlocked.
> 
> > Note that shut_down_emacs() calls Fdo_auto_save() just before
> > unlock_all_files() and that call succeeds. Fdo_auto_save() also calls
> > report_file_errno, throwing an errno 13 (Permission denied), but that
> > recovers and continues.
> 
> Presumably shut_down_emacs should recover and continue if 
> unlock_all_files fails when it directly calls unlock_all_files.

That all is true, but I think we should provide for recovery in a way
that will work with _any_ command that calls unlock_file or
unlock_all_files.  Not just these two instances.  See my other message
about this.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-10 22:39           ` Matt Armstrong
@ 2021-02-12  7:43             ` Eli Zaretskii
  2021-02-12  9:36               ` Paul Eggert
  0 siblings, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-12  7:43 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: gmatta, 46397, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: Paul Eggert <eggert@cs.ucla.edu>,  gmatta@gmail.com,
>   46397@debbugs.gnu.org,  craven@gmx.net
> Date: Wed, 10 Feb 2021 14:39:36 -0800
> 
> > We should not ignore these errors, we should ask the user what to do
> > about them.  The user can tell us the error can be ignored, but we
> > should not decide that without asking.
> 
> I think Paul's commit is a good one. I'll try to explain why.
> 
> The commit does not silently ignore ENOTDIR. Instead, it is explicitly
> handles that particular error code it in a way that honors the lock file
> API contract.

I said "silently" because the user is left unaware of what Emacs did
in this case.  We don't even show a warning or any other informative
message.

> In this case, Paul's commit changes the current_lock_owner() function
> such that it returns zero upon ENOTDIR. The caller must interpret the
> zero return as meaning "at the time current_lock_owner() was called,
> nobody owned the lock file...or the lock file was obsolete."
> 
> ENOTDIR has a specific meaning that we can rely on. Both ENOENT and
> ENOTDIR imply that the file was definitely not on disk at the time of
> the call. Because of this, current_lock_owner() can safely conclude that
> nobody owned the lock.

"Definitely"? "safely"?  How do you arrive at that conclusion?

The Posix spec of 'unlink' says:

  [ENOTDIR]
      A component of the path prefix names an existing file that is
      neither a directory nor a symbolic link to a directory, or the
      path argument contains at least one non- <slash> character and
      ends with one or more trailing <slash> characters and the last
      pathname component names an existing file that is neither a
      directory nor a symbolic link to a directory.

It doesn't even say which component of the file name is not a
directory, nor does it distinguish between the two different use cases
that could cause ENOTDIR.  How can current_lock_owner decide, on these
shaky grounds alone, that nobody owned the lock, let alone do that
'safely"?

My point is that the values of errno provide too little information
for a safe decision here, one that couldn't possibly be wrong.  It
could be the scenario that triggered this bug report, but it could be
something entirely different.  We just don't know enough, and any
assumptions in this situation can definitely err.

Which is why I still think that we need to bring the user into the
loop.  Users will know what could or did happen, and even if they
don't, they are in charge of resolving the situation.  These problems
are rare enough to not make prompting the user for the appropriate
action an annoyance, so there's no good reason not to do so.  Doing so
will, as a nice bonus, also solve similar problems for any other value
of errno, once and for all.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-12  7:43             ` Eli Zaretskii
@ 2021-02-12  9:36               ` Paul Eggert
  2021-02-12 11:33                 ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Paul Eggert @ 2021-02-12  9:36 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: gmatta, 46397, craven, Matt Armstrong

On 2/11/21 11:43 PM, Eli Zaretskii wrote:
>> From: Matt Armstrong <matt@rfc20.org>

>> In this case, Paul's commit changes the current_lock_owner() function
>> such that it returns zero upon ENOTDIR. The caller must interpret the
>> zero return as meaning "at the time current_lock_owner() was called,
>> nobody owned the lock file...or the lock file was obsolete."
>>
>> ENOTDIR has a specific meaning that we can rely on. Both ENOENT and
>> ENOTDIR imply that the file was definitely not on disk at the time of
>> the call. Because of this, current_lock_owner() can safely conclude that
>> nobody owned the lock.
> 
> "Definitely"? "safely"?  How do you arrive at that conclusion?
> 
> The Posix spec of 'unlink' says:

Matt was talking about current_lock_owner, a function that does not call 
'unlink', so it's not clear why the 'unlink' spec is relevant here.

current_lock_owner eventually calls either readlinkat or (on systems 
without symlinks) openat. If either fails with ENOTDIR, some ancestor of 
the lock file is a non-directory, hence the lock file itself cannot 
possibly exist and therefore it must be the case that nobody owns the 
lock. This is true regardless of which particular ancestor is a 
non-directory. (The lockfile name does not end in "/", so we needn't 
worry about that case here.)





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-12  9:36               ` Paul Eggert
@ 2021-02-12 11:33                 ` Eli Zaretskii
  2021-02-12 23:59                   ` Matt Armstrong
  0 siblings, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-12 11:33 UTC (permalink / raw)
  To: Paul Eggert; +Cc: gmatta, 46397, craven, matt

> Cc: gmatta@gmail.com, 46397@debbugs.gnu.org, craven@gmx.net,
>  Matt Armstrong <matt@rfc20.org>
> From: Paul Eggert <eggert@cs.ucla.edu>
> Date: Fri, 12 Feb 2021 01:36:30 -0800
> 
> > The Posix spec of 'unlink' says:
> 
> Matt was talking about current_lock_owner, a function that does not call 
> 'unlink', so it's not clear why the 'unlink' spec is relevant here.

Is the description for other APIs significantly different?  If not,
then this details is not important.

> current_lock_owner eventually calls either readlinkat or (on systems 
> without symlinks) openat. If either fails with ENOTDIR, some ancestor of 
> the lock file is a non-directory, hence the lock file itself cannot 
> possibly exist and therefore it must be the case that nobody owns the 
> lock.

Nobody owns the lock _now_, but how did that come to happen?  We have
no idea.  It could be an indication that the user's files are under
attack, for example.  So silently assuming that the lock doesn't exist
is not necessarily TRT.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-12 11:33                 ` Eli Zaretskii
@ 2021-02-12 23:59                   ` Matt Armstrong
  2021-02-13  8:07                     ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-02-12 23:59 UTC (permalink / raw)
  To: Eli Zaretskii, Paul Eggert; +Cc: 46397, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> Cc: gmatta@gmail.com, 46397@debbugs.gnu.org, craven@gmx.net,
>>  Matt Armstrong <matt@rfc20.org>
>> From: Paul Eggert <eggert@cs.ucla.edu>
>> Date: Fri, 12 Feb 2021 01:36:30 -0800
>> 
>> > The Posix spec of 'unlink' says:
>> 
>> Matt was talking about current_lock_owner, a function that does not call 
>> 'unlink', so it's not clear why the 'unlink' spec is relevant here.
>
> Is the description for other APIs significantly different?  If not,
> then this details is not important.
>
>> current_lock_owner eventually calls either readlinkat or (on systems 
>> without symlinks) openat. If either fails with ENOTDIR, some ancestor of 
>> the lock file is a non-directory, hence the lock file itself cannot 
>> possibly exist and therefore it must be the case that nobody owns the 
>> lock.
>
> Nobody owns the lock _now_, but how did that come to happen?  We have
> no idea.  It could be an indication that the user's files are under
> attack, for example.  So silently assuming that the lock doesn't exist
> is not necessarily TRT.

I think this portion of the discussion is on the fringe of the important
issue. Right now, 'current_lock_owner' can return arbitrary errno
values, and if it does Emacs is in a bad situation:

 a) The user can't kill the buffer interactively.
 b) Emacs can't exit, gets in a bad state, and must be killed.

So, definitely, we need to fix that.


Separately, I continue to think Paul's recent commit is fine. With due
consideration to the issue of inferring too much about system state
based on errno inspection, ENOTDIR and ENOENT are both unambiguously
specified for the functions 'current_lock_owner' calls -- to mean the
path components that should be directories are not in fact directories
-- and so both errno values imply that the lock file isn't at the path
Emacs is has associated with the buffer.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-12  7:15             ` Eli Zaretskii
@ 2021-02-13  1:15               ` Matt Armstrong
  2021-02-13  1:26                 ` Paul Eggert
  2021-02-13  8:28                 ` Eli Zaretskii
  0 siblings, 2 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-13  1:15 UTC (permalink / raw)
  To: Eli Zaretskii, Paul Eggert; +Cc: 46397, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> Cc: 46397@debbugs.gnu.org, craven@gmx.net
>> From: Paul Eggert <eggert@cs.ucla.edu>
>> Date: Thu, 11 Feb 2021 18:20:44 -0800
>> 
>> On 2/11/21 2:14 PM, Matt Armstrong wrote:
>> 
>> > The issue isn't confined to exiting Emacs. It appears that once in a
>> > "tricky situation where the file can't be unlocked for whatever reason"
>> > Emacs will refuse to kill the buffer because unlock_file() signals an
>> > error.
>> 
>> kill-buffer already asks the user something like "Buffer XXX modified; 
>> kill anyway? (yes or no)" when killing a buffer that's been changed 
>> without being saved. Perhaps it should also ask "File XXX cannot be 
>> unlocked; kill buffer anyway? (yes or no)" if the file can't be unlocked.
>> 
>> > Note that shut_down_emacs() calls Fdo_auto_save() just before
>> > unlock_all_files() and that call succeeds. Fdo_auto_save() also calls
>> > report_file_errno, throwing an errno 13 (Permission denied), but that
>> > recovers and continues.
>> 
>> Presumably shut_down_emacs should recover and continue if 
>> unlock_all_files fails when it directly calls unlock_all_files.
>
> That all is true, but I think we should provide for recovery in a way
> that will work with _any_ command that calls unlock_file or
> unlock_all_files.  Not just these two instances.  See my other message
> about this.

Eli, I looked a bit at finding a general solution.

One obvious issue is my inexperience in the code base. The locking logic
all seems handled in C code, and I'm not yet very familiar with Emacs'
particularities. Most of my multi-decade long career has been in lower
level C++ code, so it is semi-familiar, but I haven't yet internalized
how to think about problems in the Emacs lispy-way. I keep grasping for
expressions of OO ideas that just aren't there, or at least aren't clear
to me. :)

One issue is that 'unlock_file' has about 10 callers (though over half
are from write-region).

I'm not sure how functions like 'write_region',
'restore-buffer-modified-p' and 'replace-buffer-contents' should be
handling lock and unlock failures.

I think save-buffers-kill-terminal should be modified to leave the
buffers in a state that won't trigger un-locking much later (after it is
too late to do proper UI interactions). If a user opts to not save a
buffer, then the unlock could happen immediately (and possibly surface
an error of its own). Sound good?

One problem with the above: currently buffers do not track whether Emacs
thinks they hold the lock. Normally I'd think about adding a "user chose
to ignore unlock errors" flag at that spot, but it doesn't exist.

Instead, code attempts to re-verify lock state from the file system at
every operation. Not even setting `create-lockfiles' to nil is enough to
prevent Emacs from attempting to delete them. Some mechanism to record
the user's desire to bypass lock file errors is needed.

There is also the case where 'kill_emacs' is called from a signal, which
seems to proceed directly to shutdown without prompting the user to save
buffers. For this flow, I think the only option is to "swallow" the
errors while unlocking files. The Emacs manually even allows or this
possibility (mentioning "if Emacs crashes..."). Sound good?

You have suggested showing a prompt. What question would it ask? I am
having trouble imagining ways of making the situation clear to the user.
There are also many situations:

 - unable to determine if Emacs has the lock
 - unable to acquire the lock
 - unable to release the lock

None of these are questions. What do we expect the user to actually
understand and answer to from these prompts? Or, is issuing a message
sufficient? A message would certainly simplify things.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-13  1:15               ` Matt Armstrong
@ 2021-02-13  1:26                 ` Paul Eggert
  2021-02-13  8:21                   ` Eli Zaretskii
  2021-02-13  8:28                 ` Eli Zaretskii
  1 sibling, 1 reply; 55+ messages in thread
From: Paul Eggert @ 2021-02-13  1:26 UTC (permalink / raw)
  To: Matt Armstrong, Eli Zaretskii; +Cc: 46397, craven

On 2/12/21 5:15 PM, Matt Armstrong wrote:
> You have suggested showing a prompt. What question would it ask?

How about this:

/tmp/x/y lock is invalid; assume unlocked? (yes or no)






^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-12 23:59                   ` Matt Armstrong
@ 2021-02-13  8:07                     ` Eli Zaretskii
  0 siblings, 0 replies; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-13  8:07 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <gmatta@gmail.com>
> Cc: 46397@debbugs.gnu.org, craven@gmx.net
> Date: Fri, 12 Feb 2021 15:59:40 -0800
> 
> > Nobody owns the lock _now_, but how did that come to happen?  We have
> > no idea.  It could be an indication that the user's files are under
> > attack, for example.  So silently assuming that the lock doesn't exist
> > is not necessarily TRT.
> 
> I think this portion of the discussion is on the fringe of the important
> issue.

For me, it _is_ the important issue.  This bug is just an indication
of a larger problem we have.

> Right now, 'current_lock_owner' can return arbitrary errno
> values, and if it does Emacs is in a bad situation:
> 
>  a) The user can't kill the buffer interactively.
>  b) Emacs can't exit, gets in a bad state, and must be killed.
> 
> So, definitely, we need to fix that.

So let's fix that instead of arguing, okay?

> Separately, I continue to think Paul's recent commit is fine.

I beg to differ: by solving that single aspect of a larger problem, it
makes the larger problem less urgent, and that runs high risk of
leaving it unsolved, IME.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-13  1:26                 ` Paul Eggert
@ 2021-02-13  8:21                   ` Eli Zaretskii
  0 siblings, 0 replies; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-13  8:21 UTC (permalink / raw)
  To: Paul Eggert; +Cc: gmatta, 46397, craven

> Cc: 46397@debbugs.gnu.org, craven@gmx.net
> From: Paul Eggert <eggert@cs.ucla.edu>
> Date: Fri, 12 Feb 2021 17:26:00 -0800
> 
> On 2/12/21 5:15 PM, Matt Armstrong wrote:
> > You have suggested showing a prompt. What question would it ask?
> 
> How about this:
> 
> /tmp/x/y lock is invalid; assume unlocked? (yes or no)

Yes, SGTM.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-13  1:15               ` Matt Armstrong
  2021-02-13  1:26                 ` Paul Eggert
@ 2021-02-13  8:28                 ` Eli Zaretskii
  2021-02-14  0:49                   ` Matt Armstrong
  1 sibling, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-13  8:28 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <gmatta@gmail.com>
> Cc: 46397@debbugs.gnu.org, craven@gmx.net
> Date: Fri, 12 Feb 2021 17:15:42 -0800
> 
> One obvious issue is my inexperience in the code base. The locking logic
> all seems handled in C code, and I'm not yet very familiar with Emacs'
> particularities. Most of my multi-decade long career has been in lower
> level C++ code, so it is semi-familiar, but I haven't yet internalized
> how to think about problems in the Emacs lispy-way. I keep grasping for
> expressions of OO ideas that just aren't there, or at least aren't clear
> to me. :)

Feel free to ask questions about the internals.

> One issue is that 'unlock_file' has about 10 callers (though over half
> are from write-region).
> 
> I'm not sure how functions like 'write_region',
> 'restore-buffer-modified-p' and 'replace-buffer-contents' should be
> handling lock and unlock failures.

Why not in a unified manner?  IOW, define a single function to handle
the situation where unlocking triggers some error from lower-level
APIs such as 'unlink', and call it from unlock_file no matter what was
its caller?

> I think save-buffers-kill-terminal should be modified to leave the
> buffers in a state that won't trigger un-locking much later (after it is
> too late to do proper UI interactions). If a user opts to not save a
> buffer, then the unlock could happen immediately (and possibly surface
> an error of its own). Sound good?

I don't think I understand what you are proposing here in enough
detail.  You seem to be describing several issues together, and I
don't think I understand how you propose to solve each one of them.

> One problem with the above: currently buffers do not track whether Emacs
> thinks they hold the lock. Normally I'd think about adding a "user chose
> to ignore unlock errors" flag at that spot, but it doesn't exist.

Why would we need such a flag?  Can you show a couple of use cases
where it would be necessary?

> Instead, code attempts to re-verify lock state from the file system at
> every operation. Not even setting `create-lockfiles' to nil is enough to
> prevent Emacs from attempting to delete them. Some mechanism to record
> the user's desire to bypass lock file errors is needed.

Why is such an option needed?  If we can reasonably deal with failures
to unlock each file, wouldn't that be enough?

> There is also the case where 'kill_emacs' is called from a signal, which
> seems to proceed directly to shutdown without prompting the user to save
> buffers. For this flow, I think the only option is to "swallow" the
> errors while unlocking files. The Emacs manually even allows or this
> possibility (mentioning "if Emacs crashes..."). Sound good?

But we do ask about auto-save errors in these cases, don't we?  If so,
why not ask about locks as well?

Thanks for working on this.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-13  8:28                 ` Eli Zaretskii
@ 2021-02-14  0:49                   ` Matt Armstrong
  2021-02-14 19:22                     ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-02-14  0:49 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> There is also the case where 'kill_emacs' is called from a signal,
>> which
[...]
> But we do ask about auto-save errors in these cases, don't we?  If so,
> why not ask about locks as well?

I think auto-save issues messages and warnings, but never prompts ("asks
about").

Auto save can call `display-warning' (see auto_save_error in fileio.c)
or `message' (see message1() and message_with_string() calls in same). I
can't find any prompts.

So let's settle a question before going further. Would you be okay if
lock errors were reported in the same way(s) auto-save errors are?

I should reveal: during shut down, both `message' and `display-warning'
seems to have no effect. The first thing `shut_down_emacs()' does is
inhibit display, which is probably why.

Taking this approach would be a tremendous simplification over what I
had been thiking you were asking for. Most of my confusion centers
around re-aranging code flow such that all possible errors occur, by
construction, only *before* shut_down_emacs() may be called.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-14  0:49                   ` Matt Armstrong
@ 2021-02-14 19:22                     ` Eli Zaretskii
  2021-02-14 22:16                       ` Matt Armstrong
  0 siblings, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-14 19:22 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <gmatta@gmail.com>
> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
> Date: Sat, 13 Feb 2021 16:49:43 -0800
> 
> I think auto-save issues messages and warnings, but never prompts ("asks
> about").
> 
> Auto save can call `display-warning' (see auto_save_error in fileio.c)
> or `message' (see message1() and message_with_string() calls in same). I
> can't find any prompts.
> 
> So let's settle a question before going further. Would you be okay if
> lock errors were reported in the same way(s) auto-save errors are?

I'm okay with doing the same as auto-save does when Emacs is shutting
down, yes.  As for other situations, I'm not sure we should just
display a warning.

Were you asking only about what to do during shutdown?





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-14 19:22                     ` Eli Zaretskii
@ 2021-02-14 22:16                       ` Matt Armstrong
  2021-02-15 15:09                         ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-02-14 22:16 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Matt Armstrong <gmatta@gmail.com>
>> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
>> Date: Sat, 13 Feb 2021 16:49:43 -0800
>> 
>> I think auto-save issues messages and warnings, but never prompts
>> ("asks about").
>> 
>> Auto save can call `display-warning' (see auto_save_error in
>> fileio.c) or `message' (see message1() and message_with_string()
>> calls in same). I can't find any prompts.
>> 
>> So let's settle a question before going further. Would you be okay if
>> lock errors were reported in the same way(s) auto-save errors are?
>
> I'm okay with doing the same as auto-save does when Emacs is shutting
> down, yes.  As for other situations, I'm not sure we should just
> display a warning.
>
> Were you asking only about what to do during shutdown?

I have been primarily worried about shutdown, yes.

For non-shutdown, I want to consider errors acquiring and releasing the
lock separately.

When acquiring the lock, I think a prompt is good. The user interaction
I imagine is similar to 'ask-user-about-lock' (steal/proceed/quit), but
might just be (proceed/quit).

When releasing the lock, I have a less clear opinion but I'm thinking
that warnings are better. A warning is still quite intrusive and
obvious. Maybe we don't need to decide this part now.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-14 22:16                       ` Matt Armstrong
@ 2021-02-15 15:09                         ` Eli Zaretskii
  2021-02-16  0:49                           ` Matt Armstrong
  2021-02-19 19:10                           ` Matt Armstrong
  0 siblings, 2 replies; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-15 15:09 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <gmatta@gmail.com>
> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
> Date: Sun, 14 Feb 2021 14:16:12 -0800
> 
> When releasing the lock, I have a less clear opinion but I'm thinking
> that warnings are better. A warning is still quite intrusive and
> obvious. Maybe we don't need to decide this part now.

The problem with warnings is that they can go unnoticed, unless
followed by sit-for.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-15 15:09                         ` Eli Zaretskii
@ 2021-02-16  0:49                           ` Matt Armstrong
  2021-02-16  1:55                             ` Paul Eggert
                                               ` (2 more replies)
  2021-02-19 19:10                           ` Matt Armstrong
  1 sibling, 3 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-16  0:49 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

[-- Attachment #1: Type: text/plain, Size: 1459 bytes --]

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Matt Armstrong <gmatta@gmail.com>
>> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
>> Date: Sun, 14 Feb 2021 14:16:12 -0800
>> 
>> When releasing the lock, I have a less clear opinion but I'm thinking
>> that warnings are better. A warning is still quite intrusive and
>> obvious. Maybe we don't need to decide this part now.
>
> The problem with warnings is that they can go unnoticed, unless
> followed by sit-for.

Eli, Paul,

Attached is test code for some of the scenarios we are discussing here.

My FSF papers are out of date. Mind sending me new ones?  I am in the
United States.

These tests operate by constructing buffers within directories that the
test then restricts permissions on, which induces errors from the varous
locking calls.  Each test is a FIXME that allows us to verify the
behavior change as work on bug#46397 progresses.

In doing this I discovered that Emacs silently ignores some IO errors
when constructing lock files.  Paul noticed this a while back and added
FIXME.  This patch adds a test with a corresponding FIXME.

The test isn't complete.  It relies on POSIX semantics from the
filesystem.  It might fail on Windows.  As the review progresses I can
work on handling that scenario more gracefully.

If this kind of a test is a bad idea, feel free to let me know.  I like
to have test coverage before I do code surgery, but I don't have strong
opinions here.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: add test coverage --]
[-- Type: text/x-patch, Size: 5126 bytes --]

From 99577c5a0b582e6d0c3d9fd495dec9f27d0cb7a2 Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Mon, 15 Feb 2021 12:59:08 -0800
Subject: [PATCH] Add some test coverage for src/filelock.c (Bug#46397).

* test/src/filelock-tests.el: new file
---
 test/src/filelock-tests.el | 115 +++++++++++++++++++++++++++++++++++++
 1 file changed, 115 insertions(+)
 create mode 100644 test/src/filelock-tests.el

diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
new file mode 100644
index 0000000000..8cdcec09e7
--- /dev/null
+++ b/test/src/filelock-tests.el
@@ -0,0 +1,115 @@
+;;; filelock-tests.el --- test file locking -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file tests code in src/filelock.c and, to some extent, the
+;; related code in src/fileio.c.
+
+;;; Code:
+
+(require 'ert)
+
+(defun filelock-tests--call-with-fixture (body)
+  (let ((test-dir (make-temp-file "filelock-tests-" t)))
+    (unwind-protect
+        (let ((file-name (concat (file-name-as-directory test-dir))))
+          (with-temp-buffer
+            (setq-local buffer-file-name file-name)
+            (setq-local buffer-file-truename file-name)
+            (unwind-protect
+                (save-current-buffer
+                  (let ((create-lockfiles t))
+                    (funcall body)))
+              (set-buffer-modified-p nil))))
+      (delete-directory test-dir t nil))))
+
+(defmacro filelock-tests--with-fixture (&rest body)
+  "Create a test fixture and evaluate BODY there like `progn'.
+Create a temporary directory, then create a temporary buffer and
+set it the variable `buffer-file-name' and `buffer-file-truename'
+to it, let bind `create-lockfiles' to 't' and evaluate BODY."
+  (declare (indent 0))
+  `(filelock-tests--call-with-fixture
+    (lambda () ,@body)))
+
+(defun filelock-tests--call-with-directory-modes (modes body)
+  (let* ((dir (file-name-directory (buffer-file-name)))
+         (original-modes (file-modes dir)))
+    (set-file-modes dir modes)
+    (unwind-protect
+        (funcall body)
+      (set-file-modes dir original-modes))))
+
+(defmacro filelock-tests--with-directory-modes (modes &rest body)
+  "Evaluate BODY forms with directory MODES set.
+The directory is taken from the buffer local variable
+`buffer-file-name'.  Afterwards, restore the modes to what they
+were."
+  (declare (indent 1))
+  `(filelock-tests--call-with-directory-modes
+    ,modes (lambda () ,@body)))
+
+(ert-deftest filelock-tests-lock-buffer-permission-denied ()
+  "Check that locking a buffer in a directory with no write
+permissions does not work."
+  (filelock-tests--with-fixture
+      (should (not (file-locked-p (buffer-file-name))))
+     (filelock-tests--with-directory-modes #o500
+       ;; FIXME: (lock-buffer) should raise some sort of
+       ;; error/warning here that we should verify in test.
+       ;; Currently lock_file() ignores errors from lock_if_free().
+       ;; See the related FIXME in filelock.c.
+       (lock-buffer)
+       (should (not (file-locked-p (buffer-file-name)))))))
+
+(ert-deftest filelock-tests-file-locked-p-permission-denied ()
+  "Check that `file-locked-p' fails if the directory is inaccesible."
+  (filelock-tests--with-fixture
+    (filelock-tests--with-directory-modes #o000
+      (set-buffer-modified-p t)
+      ;; FIXME: Bug#46397: `file-locked-p' currently signals errors,
+      ;; but this prevents the user from killing the buffer.  It also
+      ;; prevents Emacs shutdown.
+      (should
+       (equal
+        (butlast (should-error (file-locked-p (buffer-file-name)))
+                 2)
+        '(file-error "Testing file lock"))))))
+
+(ert-deftest filelock-tests-unlock-permission-denied ()
+  "Check that `unlock-buffer' fails in directories that cannot be
+modified."
+  (filelock-tests--with-fixture
+    (set-buffer-modified-p t)
+    (lock-buffer)
+    (should (file-locked-p (buffer-file-name)))
+    (filelock-tests--with-directory-modes #o500
+      ;; FIXME: Bug#46397: `unlock-buffer' currently signals errors,
+      ;; but this prevents the user from killing the buffer.  It also
+      ;; prevents Emacs shutdown.
+      (should
+       (equal
+        (butlast (should-error (unlock-buffer))
+                 2)
+        '(file-error "Unlocking file"))))))
+
+(provide 'filelock-tests)
+
+;;; filelock-tests.el ends here
-- 
2.30.0


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-16  0:49                           ` Matt Armstrong
@ 2021-02-16  1:55                             ` Paul Eggert
  2021-02-16 15:06                               ` Eli Zaretskii
  2021-02-16 11:53                             ` Lars Ingebrigtsen
  2021-02-22 19:24                             ` bug#46397: [PATCH] " Matt Armstrong
  2 siblings, 1 reply; 55+ messages in thread
From: Paul Eggert @ 2021-02-16  1:55 UTC (permalink / raw)
  To: Matt Armstrong, Eli Zaretskii; +Cc: 46397, craven

On 2/15/21 4:49 PM, Matt Armstrong wrote:
> If this kind of a test is a bad idea, feel free to let me know.

It's very much a good idea, since it helps prevent similar problems from 
recurring. Thanks.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-16  0:49                           ` Matt Armstrong
  2021-02-16  1:55                             ` Paul Eggert
@ 2021-02-16 11:53                             ` Lars Ingebrigtsen
  2021-02-22 19:24                             ` bug#46397: [PATCH] " Matt Armstrong
  2 siblings, 0 replies; 55+ messages in thread
From: Lars Ingebrigtsen @ 2021-02-16 11:53 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: eggert, 46397, craven

Matt Armstrong <gmatta@gmail.com> writes:

> My FSF papers are out of date. Mind sending me new ones?  I am in the
> United States.

Here's the form to get started:


Please email the following information to assign@gnu.org, and we
will send you the assignment form for your past and future changes.

Please use your full legal name (in ASCII characters) as the subject
line of the message.
----------------------------------------------------------------------
REQUEST: SEND FORM FOR PAST AND FUTURE CHANGES

[What is the name of the program or package you're contributing to?]
Emacs

[Did you copy any files or text written by someone else in these changes?
Even if that material is free software, we need to know about it.]

[Do you have an employer who might have a basis to claim to own
your changes?  Do you attend a school which might make such a claim?]

[For the copyright registration, what country are you a citizen of?]

[What year were you born?]

[Please write your email address here.]

[Please write your postal address here.]

[Which files have you changed so far, and which new files have you written
so far?]






^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-16  1:55                             ` Paul Eggert
@ 2021-02-16 15:06                               ` Eli Zaretskii
  0 siblings, 0 replies; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-16 15:06 UTC (permalink / raw)
  To: Paul Eggert; +Cc: gmatta, 46397, craven

> Cc: 46397@debbugs.gnu.org, craven@gmx.net
> From: Paul Eggert <eggert@cs.ucla.edu>
> Date: Mon, 15 Feb 2021 17:55:00 -0800
> 
> On 2/15/21 4:49 PM, Matt Armstrong wrote:
> > If this kind of a test is a bad idea, feel free to let me know.
> 
> It's very much a good idea, since it helps prevent similar problems from 
> recurring. Thanks.

I agree.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-15 15:09                         ` Eli Zaretskii
  2021-02-16  0:49                           ` Matt Armstrong
@ 2021-02-19 19:10                           ` Matt Armstrong
  2021-02-19 19:23                             ` Eli Zaretskii
  2021-02-19 19:45                             ` Paul Eggert
  1 sibling, 2 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-19 19:10 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Matt Armstrong <gmatta@gmail.com>
>> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
>> Date: Sun, 14 Feb 2021 14:16:12 -0800
>> 
>> When releasing the lock, I have a less clear opinion but I'm thinking
>> that warnings are better. A warning is still quite intrusive and
>> obvious. Maybe we don't need to decide this part now.
>
> The problem with warnings is that they can go unnoticed, unless
> followed by sit-for.

Hello again Eli, Paul,

I've got a more concrete plan and would like some feedback on it.

While waiting for my signed FSF papers to be acknowledged I have been
investigating how to achieve a prompt in some depth.  I'm coming to the
opinion that issuing a prompt from `unlock-buffer' itself is a bad idea,
but I think prompting from `kill-buffer' is okay.  I could write a whole
essay about why, but instead I'll just propose the following and ask for
your thoughts:

 (a) Modify `kill-buffer' to call `unlock-buffer' sooner, closer to the
     point where it is already running hooks prompting the user.  Handle
     unlock errors there by prompting.  This function already prompts
     the user for the "buffer modified, save anyway" case so a new
     prompt there feels fine.

 (b) Modify Emacs shutdown to handle `unlock-buffer` errors differently
     as well (with `message' or `display-warning').

 (c) Modify `unlock-buffer' to include the buffer name in the errors it
     signals.  It currently only includes the buffer's
     `buffer-true-filename'.  With this the user can more easily find
     and kill the offending buffer.

This approach solves the concrete problems we know about, but doesn't
dramatically change Emacs behavior in other ways.  In other words, Emacs
typically signals errors for file system errors and that stays the same.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-19 19:10                           ` Matt Armstrong
@ 2021-02-19 19:23                             ` Eli Zaretskii
  2021-02-19 21:46                               ` Matt Armstrong
  2021-02-19 19:45                             ` Paul Eggert
  1 sibling, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-19 19:23 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
> Date: Fri, 19 Feb 2021 11:10:45 -0800
> 
> I'm coming to the opinion that issuing a prompt from `unlock-buffer'
> itself is a bad idea, but I think prompting from `kill-buffer' is
> okay.

What do you propose to do for all the other users of unlock-buffer?

> I could write a whole essay about why, but instead I'll just
> propose the following and ask for your thoughts:
> 
>  (a) Modify `kill-buffer' to call `unlock-buffer' sooner, closer to the
>      point where it is already running hooks prompting the user.

Why do we need to move the call?  Can we leave it in its current
place, and thus minimize potential unintended problems this could
cause?

Thanks.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-19 19:10                           ` Matt Armstrong
  2021-02-19 19:23                             ` Eli Zaretskii
@ 2021-02-19 19:45                             ` Paul Eggert
  2021-02-19 21:52                               ` Matt Armstrong
  2021-03-08  2:18                               ` Matt Armstrong
  1 sibling, 2 replies; 55+ messages in thread
From: Paul Eggert @ 2021-02-19 19:45 UTC (permalink / raw)
  To: Matt Armstrong, Eli Zaretskii; +Cc: 46397, craven

On 2/19/21 11:10 AM, Matt Armstrong wrote:
>   (a) Modify `kill-buffer' to call `unlock-buffer' sooner, closer to the
>       point where it is already running hooks prompting the user.  Handle
>       unlock errors there by prompting.

How would unlock-buffer's API change, so that kill-buffer and 
unlock-buffer's other callers can do the right thing? Would it signal an 
error, and if so which one (or a new kind of error)?






^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-19 19:23                             ` Eli Zaretskii
@ 2021-02-19 21:46                               ` Matt Armstrong
  2021-02-20  9:09                                 ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-02-19 21:46 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Matt Armstrong <matt@rfc20.org>
>> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
>> Date: Fri, 19 Feb 2021 11:10:45 -0800
>> 
>> I'm coming to the opinion that issuing a prompt from `unlock-buffer'
>> itself is a bad idea, but I think prompting from `kill-buffer' is
>> okay.
>
> What do you propose to do for all the other users of unlock-buffer?

They continue to signal errors.

I would be happy to send a list of reasons why I think this is a safer
thing to do than prompting.  (reasons that I admit I could be misguided)


>> I could write a whole essay about why, but instead I'll just
>> propose the following and ask for your thoughts:
>> 
>>  (a) Modify `kill-buffer' to call `unlock-buffer' sooner, closer to the
>>      point where it is already running hooks prompting the user.
>
> Why do we need to move the call?  Can we leave it in its current
> place, and thus minimize potential unintended problems this could
> cause?

In part because `kill-buffer' currently calls `unlock-buffer' after this
comment:

  /* We have no more questions to ask.  Verify that it is valid
     to kill the buffer.  This must be done after the questions
     since anything can happen within do_yes_or_no_p.  */

(This class of problem is also one of the reasons for my answer above.)





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-19 19:45                             ` Paul Eggert
@ 2021-02-19 21:52                               ` Matt Armstrong
  2021-03-08  2:18                               ` Matt Armstrong
  1 sibling, 0 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-19 21:52 UTC (permalink / raw)
  To: Paul Eggert, Eli Zaretskii; +Cc: 46397, craven

Paul Eggert <eggert@cs.ucla.edu> writes:

> On 2/19/21 11:10 AM, Matt Armstrong wrote:
>>   (a) Modify `kill-buffer' to call `unlock-buffer' sooner, closer to the
>>       point where it is already running hooks prompting the user.  Handle
>>       unlock errors there by prompting.
>
> How would unlock-buffer's API change, so that kill-buffer and 
> unlock-buffer's other callers can do the right thing? Would it signal an 
> error, and if so which one (or a new kind of error)?

unlock-buffer signals file errors today, and that wouldn't change under
this idea.

I suppose we could invent a new 'file-unlock-error if we think the
caller may want to discriminate, but so far my prototype in `kill-buffer
just handles any 'file-error that `unlock-buffer' happens to signal.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-19 21:46                               ` Matt Armstrong
@ 2021-02-20  9:09                                 ` Eli Zaretskii
  2021-02-21  0:36                                   ` Matt Armstrong
  0 siblings, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-20  9:09 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
> Date: Fri, 19 Feb 2021 13:46:27 -0800
> 
> >> I'm coming to the opinion that issuing a prompt from `unlock-buffer'
> >> itself is a bad idea, but I think prompting from `kill-buffer' is
> >> okay.
> >
> > What do you propose to do for all the other users of unlock-buffer?
> 
> They continue to signal errors.
> 
> I would be happy to send a list of reasons why I think this is a safer
> thing to do than prompting.  (reasons that I admit I could be misguided)

I think we should audit all the callers of unlock_buffer and
unlock_file, and see if signaling an error there is really the best
alternative.  I still think that prompting the user for what to do,
with one of the possible responses being "ignore", could be a better
solution, at least in some of these cases, because signaling an error
means the results of some operation are lost.  For example, consider
replace-buffer-contents, which is a command -- we could signal the
error there after everything, all the heavy processing, has been done
already.  Is that reasonable?  Or are you relying on the ability of
unlock_file to silently ignore the errors where the user should choose
"ignore"?  Because I'd like to explicitly NOT rely on that.

So yes, a list of callers and the reasons not to prompt the user there
would be a good starting point, TIA.

> >>  (a) Modify `kill-buffer' to call `unlock-buffer' sooner, closer to the
> >>      point where it is already running hooks prompting the user.
> >
> > Why do we need to move the call?  Can we leave it in its current
> > place, and thus minimize potential unintended problems this could
> > cause?
> 
> In part because `kill-buffer' currently calls `unlock-buffer' after this
> comment:
> 
>   /* We have no more questions to ask.  Verify that it is valid
>      to kill the buffer.  This must be done after the questions
>      since anything can happen within do_yes_or_no_p.  */

OK, but then the call to unlock_buffer should have all the conditions
tested later, under which we will NOT kill the buffer.  Because
otherwise we could pop the question for a buffer that we are not going
to kill.

> (This class of problem is also one of the reasons for my answer above.)

When the alternative is worse than what could possibly happen inside
do_yes_or_no_p, we may decide to ask the question anyway.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-20  9:09                                 ` Eli Zaretskii
@ 2021-02-21  0:36                                   ` Matt Armstrong
  2021-02-21 23:43                                     ` Mike Kupfer
  2021-02-24 17:37                                     ` Matt Armstrong
  0 siblings, 2 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-21  0:36 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Matt Armstrong <matt@rfc20.org>
>> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
>> Date: Fri, 19 Feb 2021 13:46:27 -0800
>> 
>> >> I'm coming to the opinion that issuing a prompt from `unlock-buffer'
>> >> itself is a bad idea, but I think prompting from `kill-buffer' is
>> >> okay.
>> >
>> > What do you propose to do for all the other users of unlock-buffer?
>> 
>> They continue to signal errors.
>> 
>> I would be happy to send a list of reasons why I think this is a safer
>> thing to do than prompting.  (reasons that I admit I could be misguided)
>
> I think we should audit all the callers of unlock_buffer and
> unlock_file, and see if signaling an error there is really the best
> alternative.

I had already started something like this so I finished it off.  See the
end of this email.  It is long.


> I still think that prompting the user for what to do, with one of the
> possible responses being "ignore", could be a better solution, at
> least in some of these cases, because signaling an error means the
> results of some operation are lost.

I share the concern about losing work due to unlock errors.  I become
annoyed any time a program fails or asks me questions for unecessary
reasons.


> For example, consider replace-buffer-contents, which is a command --
> we could signal the error there after everything, all the heavy
> processing, has been done already.  Is that reasonable?

I think I can agree that this kind of detail oriented approach can
definitely make individual functions better at leaving the data
structures they modify in well defined states, even in the face of
error.

I am not sure how to apply this idea to this discussion, however.  It
seems like a more general observation about writing "exception safe"
code.

Or perhaps I am missing what you are saying?


> Or are you relying on the ability of unlock_file to silently ignore
> the errors where the user should choose "ignore"?  Because I'd like to
> explicitly NOT rely on that.

My current idea is to keep unlock file exactly the same, in that it
signals errors the way it does today.  Then, surgically improve the only
two places in Emacs where we know this is a serious problem.

My other idea was to change the unlock functions to never signal an
errors, but instead report them through `display-warning', perhaps with
warning level :error.  This is still my personally preferred option, but
I know that you don't like it.

Still another idea is to modify `unlock_file' itself to issue a user
prompt.  This is by far my least favorite approach because the
implications of such a large change for such a comparatively minor issue
terrify me.

I should say, in case I haven't before, that I am quite willing to put
my preferences aside here.  I have no experience maintaining Emacs.

I do have a great deal of experience maintaining large, very complex but
also robust server based systems.  There, being very "loud and obvious"
for every anomolous condition that could possibly be a problem just
doesn't work -- it is just too much information.  In systems large
enough the "rare" things are basically always happening!  With that kind
of software it tends to be most important to limit reported errors to
truly important issues that indicate a failure to satisfy requests
within well defined criteria.  The "softer" anomolous conditions are
demoted to warnings that are available for diagnostic analysis if
needed.  The approach, done right, provides the correct level of detail
to diagnose problems when the need arises.

I will say that this design conversation has been quite a surprise.  In
my former "server based" world I would have sent a code review to demote
unlock errors to `display-error' and it would have been approved with
little discussion!  We did that kind of thing all the time, and it was
just "undersood" best practice.  :-)


> So yes, a list of callers and the reasons not to prompt the user there
> would be a good starting point, TIA.

Could prompting from essentially arbitrary places in Emacs be made safer
with a modal `reach-char' based approach?  This is what userlock.el does
in `ask-user-about-lock'.  It avoids the "anything can happen here"
issue that non-modal prompts have, so it feels like a safer thing to do
if it ends up that we decide to make "unlock buffer" and "unlock file"
prompt the user.

Prompts, whether model or not, are difficult for me to assess in terms
of risks.  At this point I definitely value the intuition of long time
Emacs hackers over my own ability to read and reason about Emacs code.

A second issue is what to ask the user.  Prompting the user to either
ignore or signal an error feels quite low level.  Paul suggested:

     /tmp/x/y lock is invalid; assume unlocked? (yes or no)

But I would argue that the consequences of answering "yes" or "no" to
that kind of question are not clear.

Not to beat a dead horse, but a *Warning* buffer based approach from
`display-error' would be clearer.  Something like:

  Warning (unlock-file): Could not unlock buffer <myfile>,
  Permission Denied, /blah/blah/.#myfile

For one, the command the user issued would succeed, with no extra
interaction required from them, so they'd immediately know that things
worked, but for this one strange issue.  This is similar to auto-save in
intent.  Both auto save and lock file management happen "in the
background" -- out of sight, and out of mind until they are needed.
They aren't things the user manages, or should be asked to manage errors
from, IMO.


>> >>  (a) Modify `kill-buffer' to call `unlock-buffer' sooner, closer to the
>> >>      point where it is already running hooks prompting the user.
>> >
>> > Why do we need to move the call?  Can we leave it in its current
>> > place, and thus minimize potential unintended problems this could
>> > cause?
>> 
>> In part because `kill-buffer' currently calls `unlock-buffer' after this
>> comment:
>> 
>>   /* We have no more questions to ask.  Verify that it is valid
>>      to kill the buffer.  This must be done after the questions
>>      since anything can happen within do_yes_or_no_p.  */
>
> OK, but then the call to unlock_buffer should have all the conditions
> tested later, under which we will NOT kill the buffer.  Because
> otherwise we could pop the question for a buffer that we are not going
> to kill.
>
>> (This class of problem is also one of the reasons for my answer above.)
>
> When the alternative is worse than what could possibly happen inside
> do_yes_or_no_p, we may decide to ask the question anyway.

Yes, we're often forced to choose the "least worse" option.


Returning to the code audit:

> I think we should audit all the callers of unlock_buffer and
> unlock_file, and see if signaling an error there is really the best
> alternative.

This is a difficult analysis due to the number call sites.  In emacs.git
there are 1600 call sites to lisp level functions that might call
`unlock-file'.

I'll use `func' for Lisp functions and func() for C functions.  The call
counts I cite below are limited to callers in emacs.git.

I looked at call sites of only the core level commands.  There are too
many indirect callers to do more than that.  In each call site, I
thought about whether the code looked like it had obvious problems if
errors were signaled from calls that unlocked, and also whether the code
might have especially delicate invariants that might break if code
entered the kind of quasi "recursive edit" operation that a non-modal
prompts imply.

unlock_file() is an implementation detail at the C level, which has a
few major callers:

`write-region' can open/close the destination file, locking/unlocking it
as well.  This function also provides LOCKNAME, an Emacs Lisp level
override for the lock file name.  It is has ~500 call sites.

`insert-file-contents' usually takes a file from unlocked->locked and
leaves it locked, but it will unlock if it turns out the buffer contents
did not change.  It also calls unlock_file() in certain cases when
`find-file-name-handler' returns nil (why I don't quite understand).  It
has ~750 call sites.

`replace-buffer-contents' is similar: it locks but will then unlock if
the buffer contents do not change.  It has 16 call sites.

`restore-buffer-modified-p' will unlock the file if the buffer becomes
unmodified.  It has ~51 call sites.

`set-buffer-modified-p` will unlock if passed 'nil'.  There are ~400
call sites with ~290 of them matching the literal expression
(set-buffer-modified-p nil).

`unlock-buffer' does the obvious, with few direct call sites, so I
looked at them all.  Some were redundant -- occuring just before or
after (set-buffer-modified nil).  The rest:

	* lisp/jka-compr.el (jka-compr-insert-file-contents): No obvious
          (to me) problem if `unlock-file' signals an error.

	* lisp/simple.el (primitive-undo): resuming after arbitrary
          interactions (i.e. a non-modal prompt) might screw up the undo
          invariants?

	* lisp/mh-e/mh-show.el (mh-clean-msg-header, mh-unvisit-file):
    	  hard to say, very old code...
	* lisp/mh-e/mh-comp.el (mh-read-draft): ditto.

	* lisp/files.el (find-alternate-file): Signaled errors from
	(unlock-file) look okay.  It occurs under an (unwind-protect...)
	which has UNWINDFORMS that look good to me.
	(set-visited-file-name): No obvious problem.
	(revert-buffer-insert-file-contents--default-function): No obvious
	problems.

	* lisp/simple.el (primitive-undo): Signaled unlock errors may
	case broken undo invariants?  I am not too worried, since undo
	can also lock the file.






^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-21  0:36                                   ` Matt Armstrong
@ 2021-02-21 23:43                                     ` Mike Kupfer
  2021-02-22  1:42                                       ` Matt Armstrong
  2021-02-24 17:37                                     ` Matt Armstrong
  1 sibling, 1 reply; 55+ messages in thread
From: Mike Kupfer @ 2021-02-21 23:43 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: eggert, 46397, craven, wohler

(Adding Bill Wohler, who has a better grasp than I about why MH-E does
some things.)

Matt Armstrong wrote:

> Eli Zaretskii <eliz@gnu.org> writes:
> 
> > I think we should audit all the callers of unlock_buffer and
> > unlock_file, and see if signaling an error there is really the best
> > alternative.
[...]
> 	* lisp/mh-e/mh-show.el (mh-clean-msg-header, mh-unvisit-file):
>     	  hard to say, very old code...
> 	* lisp/mh-e/mh-comp.el (mh-read-draft): ditto.

I'm not sure I completely understanding the logic behind those calls to
unlock-buffer, but I'll take a stab at it.

mh-unvisit-file: this function's purpose in life is to disassociate the
current buffer from the underlying file.  (MH-E reuses a per-folder
buffer when displaying a message.)

mh-read-draft: this case is similar to mh-unvisit-file.  In the relevant
code path, the user's configuration allows for only a single "draft"
buffer, and MH-E is cleaning up a potentially old draft buffer for
reuse.

mh-clean-msg-header: this one confuses me.  mh-clean-msg-header edits
the current buffer to reflect the user's preferences for which mail
headers to display.  But there are several places where
mh-clean-msg-header is used, and I haven't figured out if the buffer
ever has an associated file when mh-clean-msg-header is called.  So I'm
not even sure the call to unlock-buffer actually has any value here.

But to Eli's question, I think a signal is fine for MH-E if the lockfile
can't be removed for some reason.  An uncaught signal could leave the
current buffer in an odd state, but the user can simply kill the buffer
and retry whatever operation she had attempted.  Or if the buffer has
something that is worth saving, the user can attempt to save the buffer
somewhere, perhaps a different filesystem (e.g., if the original
filesystem went read-only due to the OS detecting a problem).

I don't understand the proposal for unlock-buffer (or something under
it) to prompt the user.  IIUC, the proposal is for a prompt like

> /tmp/x/y lock is invalid; assume unlocked? (yes or no)

I assume that if the user responds with "yes", unlock-buffer returns and
the caller is none the wiser.  If the user responds with "no", what
happens?

mike





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-21 23:43                                     ` Mike Kupfer
@ 2021-02-22  1:42                                       ` Matt Armstrong
  2021-03-14 18:03                                         ` Bill Wohler
  0 siblings, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-02-22  1:42 UTC (permalink / raw)
  To: Mike Kupfer; +Cc: eggert, 46397, craven, wohler

Mike Kupfer <mkupfer@alum.berkeley.edu> writes:

> (Adding Bill Wohler, who has a better grasp than I about why MH-E does
> some things.)
>
> Matt Armstrong wrote:
>
>> Eli Zaretskii <eliz@gnu.org> writes:
>> 
>> > I think we should audit all the callers of unlock_buffer and
>> > unlock_file, and see if signaling an error there is really the best
>> > alternative.
> [...]
>> 	* lisp/mh-e/mh-show.el (mh-clean-msg-header, mh-unvisit-file):
>>     	  hard to say, very old code...
>> 	* lisp/mh-e/mh-comp.el (mh-read-draft): ditto.
>
> I'm not sure I completely understanding the logic behind those calls to
> unlock-buffer, but I'll take a stab at it.

[...]

Thanks for those analysis Mike.  They make sense to me.


> But to Eli's question, I think a signal is fine for MH-E if the
> lockfile can't be removed for some reason.  An uncaught signal could
> leave the current buffer in an odd state, but the user can simply kill
> the buffer and retry whatever operation she had attempted.

Yes, and this bug is at least in part about the behavior of
`kill-buffer' when faced with the same issue.  That and `kill-emacs'.


> Or if the buffer has something that is worth saving, the user can
> attempt to save the buffer somewhere, perhaps a different filesystem
> (e.g., if the original filesystem went read-only due to the OS
> detecting a problem).

I think the "buffer has something worth saving" case is not a concern.

The calls to `unlock-buffer' all occur after the buffer contents have
been saved, or otherwise marked as unmodified, or, in the case of
`kill-buffer', after the user has chosen to not save a modified buffer.


> I don't understand the proposal for unlock-buffer (or something under
> it) to prompt the user.  IIUC, the proposal is for a prompt like
>
>> /tmp/x/y lock is invalid; assume unlocked? (yes or no)
>
> I assume that if the user responds with "yes", unlock-buffer returns
> and the caller is none the wiser.  If the user responds with "no",
> what happens?
>
> mike

I think under the current idea, in the case of `kill-buffer', answering
"no" from the prompt the buffer un-killed.  I think the technical
mechanism would be to either re-signal the underlying 'file-error or
signal a new 'unlock-error that contains similar information.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: [PATCH] Re: bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-16  0:49                           ` Matt Armstrong
  2021-02-16  1:55                             ` Paul Eggert
  2021-02-16 11:53                             ` Lars Ingebrigtsen
@ 2021-02-22 19:24                             ` Matt Armstrong
  2 siblings, 0 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-22 19:24 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

[-- Attachment #1: Type: text/plain, Size: 136 bytes --]

Tags: patch

Attached is a rev of my prior patch to add some test coverage for
src/filelock.c.

P.S. my FSF copyright papers are done.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-some-test-coverage-for-src-filelock.c-Bug-46397.patch --]
[-- Type: text/x-patch, Size: 6780 bytes --]

From 18529d097624e0d4647cf04118d4ce98adc474ba Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Mon, 15 Feb 2021 12:59:08 -0800
Subject: [PATCH] Add some test coverage for src/filelock.c (Bug#46397).

* test/src/filelock-tests.el: New file.
---
 test/src/filelock-tests.el | 148 +++++++++++++++++++++++++++++++++++++
 1 file changed, 148 insertions(+)
 create mode 100644 test/src/filelock-tests.el

diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
new file mode 100644
index 0000000000..d9534e2d16
--- /dev/null
+++ b/test/src/filelock-tests.el
@@ -0,0 +1,148 @@
+;;; filelock-tests.el --- test file locking -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file tests code in src/filelock.c and related buffer and file
+;; I/O code.
+
+;;; Code:
+
+(require 'ert)
+
+(defun filelock-tests--supported-p ()
+  "Return t if the current system implements file locking."
+  (not (memq system-type '(ms-dos))))
+
+(defun filelock-tests--call-with-fixture (body)
+  "Call BODY under a standard test fixture.
+See `filelock-tests--with-fixture'."
+  (let* ((test-dir (make-temp-file "filelock-tests-" t))
+         (test-dir-modes (file-modes test-dir)))
+    (unwind-protect
+        (let ((file-name (file-truename
+                          (concat (file-name-as-directory test-dir)
+                                  "file"))))
+          (with-temp-buffer
+            (let ((temp-buffer (current-buffer)))
+              ;; Setting both file names is sufficient to enable file
+              ;; locks.
+              (setq buffer-file-name file-name
+                    buffer-file-truename file-name)
+              (unwind-protect
+                  (let ((create-lockfiles t))
+                    (funcall body))
+                (set-file-modes test-dir test-dir-modes)
+                (when (buffer-live-p temp-buffer)
+                  (with-current-buffer temp-buffer
+                    (set-buffer-modified-p nil)))))))
+      (delete-directory test-dir t nil))))
+
+(defmacro filelock-tests--with-fixture (&rest body)
+  "Create a test fixture evaluate BODY there like `progn'.
+Create a temporary directory, then create a temporary buffer and
+set it the variable `buffer-file-name' and `buffer-file-truename'
+to it, let bind `create-lockfiles' to 't' and evaluate BODY.
+Finally, delete the temporary directory."
+  (declare (indent 0))
+  `(filelock-tests--call-with-fixture
+    (lambda () ,@body)))
+
+(ert-deftest filelock-tests-when-create-lockfiles-nil ()
+  (filelock-tests--with-fixture
+   (let ((create-lockfiles nil))
+     (should (not (file-locked-p buffer-file-truename)))
+     (insert "some text")
+     (lock-buffer)
+     (should (not (file-locked-p buffer-file-truename))))))
+
+(ert-deftest filelock-tests-when-create-lockfiles-t ()
+  (filelock-tests--with-fixture
+    (should (equal t create-lockfiles))
+    (insert "some text")
+    (lock-buffer)
+    ;; Verify 't' explicitly and not any true value; 't' alone means
+    ;; the current process owns the lock.
+    (let ((expected (if (filelock-tests--supported-p)
+                        t
+                      nil)))
+      (should (equal expected (file-locked-p buffer-file-truename)))
+      (unlock-buffer)
+      (should (equal nil (file-locked-p buffer-file-truename))))))
+
+(ert-deftest filelock-tests-file-locked-p-inaccessible-dir ()
+  (skip-unless (filelock-tests--supported-p))
+  (filelock-tests--with-fixture
+    (set-file-modes (file-name-directory (buffer-file-name))
+                    (file-modes-symbolic-to-number "ugo="))
+    (set-buffer-modified-p t)
+    (let ((full-error (should-error (file-locked-p (buffer-file-name))
+                                    :type 'file-error)))
+      (should (equal "Testing file lock" (nth 1 full-error))))))
+
+(ert-deftest filelock-tests-lock-buffer-inaccessible-dir ()
+  (skip-unless (filelock-tests--supported-p))
+  (filelock-tests--with-fixture
+    (let* ((test-dir (file-name-directory (buffer-file-name)))
+           (original-modes (file-modes test-dir)))
+      (set-file-modes test-dir (file-modes-symbolic-to-number "ugo="))
+      (insert "some text")
+      ;; FIXME: (lock-buffer) should signal an error here.
+      (lock-buffer)
+      (set-file-modes test-dir original-modes)
+      (should (equal nil (file-locked-p (buffer-file-name)))))))
+
+(ert-deftest filelock-tests-lock-buffer-dir-not-writeable ()
+  (skip-unless (filelock-tests--supported-p))
+  (filelock-tests--with-fixture
+    (set-file-modes (file-name-directory (buffer-file-name))
+                    (file-modes-symbolic-to-number "u=rx"))
+    (insert "some text")
+    ;; FIXME: (lock-buffer) should signal here, but lock_file() in
+    ;; filelock.c ignores some file system errors; see the related
+    ;; FIXME there.
+    (lock-buffer)
+    (should (equal nil (file-locked-p (buffer-file-name))))))
+
+(ert-deftest filelock-tests-unlock-buffer-inaccessible-dir ()
+  (skip-unless (filelock-tests--supported-p))
+  (filelock-tests--with-fixture
+    (insert "some text")
+    (lock-buffer)
+    (should (file-locked-p (buffer-file-name)))
+    (set-file-modes (file-name-directory (buffer-file-name))
+                    (file-modes-symbolic-to-number "ugo="))
+    (let ((full-error (should-error (unlock-buffer)
+                                    :type 'file-error)))
+      (should (equal "Unlocking file" (nth 1 full-error))))))
+
+(ert-deftest filelock-tests-unlock-buffer-dir-not-writeable ()
+  (skip-unless (filelock-tests--supported-p))
+  (filelock-tests--with-fixture
+    (insert "some text")
+    (should (file-locked-p (buffer-file-name)))
+    (set-file-modes (file-name-directory (buffer-file-name))
+                    (file-modes-symbolic-to-number "u=rx"))
+    (let ((full-error (should-error (unlock-buffer)
+                                    :type 'file-error)))
+      (should (equal "Unlocking file" (nth 1 full-error))))))
+
+(provide 'filelock-tests)
+
+;;; filelock-tests.el ends here
-- 
2.30.0


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-21  0:36                                   ` Matt Armstrong
  2021-02-21 23:43                                     ` Mike Kupfer
@ 2021-02-24 17:37                                     ` Matt Armstrong
  2021-02-24 18:50                                       ` Eli Zaretskii
  2021-03-01 16:59                                       ` Eli Zaretskii
  1 sibling, 2 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-02-24 17:37 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

Matt Armstrong <matt@rfc20.org> writes:

> Eli Zaretskii <eliz@gnu.org> writes:

[...]

>> I think we should audit all the callers of unlock_buffer and
>> unlock_file, and see if signaling an error there is really the best
>> alternative.

[...]

Eli, no rush but I wanted to be clear that I have paused work on this
bug until I get more feedback from you.  At this point I don't have any
more avenues that I feel I can explore, and I am unsure what more you'd
like to see before we move forward with a decision here.

I'm willing to work toward any of the options I outline below, or
something new that I have not considered.

I still like my original idea of calling display-warning for all unlock
errors, essentially turning "unlock" into a best effort function at the
API level.  I think display-warning is intrusive enough that users are
unlikely to simply not notice the problem, and there are worse things
than leaving lock files around.

I could see prompting for all errors from unlock-buffer itself, but I
have concerns about it.  As my analysis showed, the unlock operation
happens in potentially thousands of call sites in Emacs core alone.
This not counting third party elisp code.  It escallates an error to a
user interaction, which defeats mechanisms like with-demoted-errors and
other uses of condition-case.

Short of my display-warning idea, I think a reasonable option is to
handle unlock errors in targeted places.  This isn't a particularly
radical idea, I think.  The only two we know to be very problematic for
users is kill-buffer and kill-emacs.

That is my summary of where the discussion is at.  Let me know how you'd
like to proceed.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-24 17:37                                     ` Matt Armstrong
@ 2021-02-24 18:50                                       ` Eli Zaretskii
  2021-03-01 16:59                                       ` Eli Zaretskii
  1 sibling, 0 replies; 55+ messages in thread
From: Eli Zaretskii @ 2021-02-24 18:50 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
> Date: Wed, 24 Feb 2021 09:37:49 -0800
> 
> Eli, no rush but I wanted to be clear that I have paused work on this
> bug until I get more feedback from you.

Yes, I understood that.  I need to find enough time to carefully read
and review what you posted.  Sorry for being slow, but I didn't forget
nor have I dropped the ball om this.  Stay tuned.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-24 17:37                                     ` Matt Armstrong
  2021-02-24 18:50                                       ` Eli Zaretskii
@ 2021-03-01 16:59                                       ` Eli Zaretskii
  2021-03-05 22:19                                         ` Matt Armstrong
  1 sibling, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-03-01 16:59 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
> Date: Wed, 24 Feb 2021 09:37:49 -0800
> 
> I still like my original idea of calling display-warning for all unlock
> errors, essentially turning "unlock" into a best effort function at the
> API level.  I think display-warning is intrusive enough that users are
> unlikely to simply not notice the problem, and there are worse things
> than leaving lock files around.

OK, you've convinced me: let's try the warning approach.  Can you
present a patch for that, please?

As for the tests you posted: too many of them rely on Posix file
modes, and thus will probably either fail or be unable to provide
meaningful testing on MS-Windows.  Can we please augment that by tests
that create unlocking problems by, e.g., running a shell command to
remove or rename or otherwise sabotage the lock file, so that the new
functionality could be meaningfully tested on Windows as well?





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-01 16:59                                       ` Eli Zaretskii
@ 2021-03-05 22:19                                         ` Matt Armstrong
  2021-03-06  9:36                                           ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-03-05 22:19 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

[-- Attachment #1: Type: text/plain, Size: 2293 bytes --]

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Matt Armstrong <matt@rfc20.org>
>> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
>> Date: Wed, 24 Feb 2021 09:37:49 -0800
>> 
>> I still like my original idea of calling display-warning for all unlock
>> errors, essentially turning "unlock" into a best effort function at the
>> API level.  I think display-warning is intrusive enough that users are
>> unlikely to simply not notice the problem, and there are worse things
>> than leaving lock files around.
>
> OK, you've convinced me: let's try the warning approach.  Can you
> present a patch for that, please?

Thanks Eli, will do.  I have made progress with that idea, but I'd
rather get the test in first.  See patch attached below.


> As for the tests you posted: too many of them rely on Posix file
> modes, and thus will probably either fail or be unable to provide
> meaningful testing on MS-Windows.  Can we please augment that by tests
> that create unlocking problems by, e.g., running a shell command to
> remove or rename or otherwise sabotage the lock file, so that the new
> functionality could be meaningfully tested on Windows as well?

I have not been able to come up with a way to achieve what you ask for.

I have no access to an MS-Windows system to test with, though I have
been testing with small FAT and NTFS file systems in a ramdisk under
GNU/Linux.  It may be possible to take some approach that works on
MS-Windows systems, but I'm not in a good position to write that code.

Another issue is that the lock file is typicaly a symbolink link, and
operating systems typically ignore file modes on symbolic links, so it
is hard to put the lock file itself into an "invalid" state --
i.e. where simply attempting to access or delete it generates a file
system level error.  This is why I resorted to modifying file modes on
the containing directory -- attemps to remove or access files in such a
directory do reliably generate errors (on POSIX systems).

What I've done is make the tests skip themselves when `set-file-mode' on
the test's temporary directory appears to not work.  When I test this on
an ext2 file system the tests complete.  When I test this on a FAT and
NTFS file system (set up as a ramdisk on GNU/Linux), the tests skip
themselves.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-some-test-coverage-for-src-filelock.c-Bug-46397.patch --]
[-- Type: text/x-diff, Size: 8074 bytes --]

From e05d400c177333cff0f558aff1948bcd55130c17 Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Mon, 15 Feb 2021 12:59:08 -0800
Subject: [PATCH 1/2] Add some test coverage for src/filelock.c (Bug#46397).

* test/src/filelock-tests.el: new file
---
 test/src/filelock-tests.el | 183 +++++++++++++++++++++++++++++++++++++
 1 file changed, 183 insertions(+)
 create mode 100644 test/src/filelock-tests.el

diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
new file mode 100644
index 0000000000..e568050c42
--- /dev/null
+++ b/test/src/filelock-tests.el
@@ -0,0 +1,183 @@
+;;; filelock-tests.el --- test file locking -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file tests code in src/filelock.c and, to some extent, the
+;; related code in src/fileio.c.
+
+;; Note: many of these tests set file modes to unusual values, with
+;; the aim of exercising Emacs' error handling code.  These tests
+;; require POSIX file system semantics, which are not available on all
+;; file systems (e.g. FAT, NTFS).  The tests skip themselves when the
+;; file system does not support the given file modes.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'ert)
+
+(defvar filelock-tests-temporary-file-directory nil
+  "The directory for writing temporary files in filelock tests.
+This is used by the function
+`filelock-tests--make-temp-directory' to override the value of
+the variable `temporary-file-directory'.")
+
+(defun filelock-tests--make-temp-directory ()
+  "Create and return the name of a temporary directory.
+The caller should delete the directory."
+  (let ((temporary-file-directory (or filelock-tests-temporary-file-directory
+                                      temporary-file-directory)))
+    (make-temp-file "test" t)))
+
+(defun filelock-tests--fixture (test-function)
+  "Call TEST-FUNCTION under a test fixture.
+Create a test directory and a buffer whose `buffer-file-name' and
+`buffer-file-truename' are a file within it.  Call TEST-FUNCTION,
+passing it directory name.  Finally, delete the buffer and the
+test directory.
+
+This function attempts to delete the test directory even if the
+test leaves the buffer locked or the test directory with strange
+permissions."
+  (let* ((temp-dir (filelock-tests--make-temp-directory))
+         (name (concat (file-name-as-directory temp-dir)
+                       "file"))
+         (create-lockfiles t))
+    (unwind-protect
+        (with-temp-buffer
+          (setq buffer-file-name name)
+          (setq buffer-file-truename name)
+          (unwind-protect
+              (save-current-buffer
+                (funcall test-function temp-dir))
+
+            ;; Here begins somewhat delicate cleanup logic.  It must
+            ;; be performed in this order.
+            ;;
+            ;; First, give Emacs permission to modify the test
+            ;; directory.  The test may have left the diretory's modes
+            ;; in an unusual state.
+            ;;
+            ;; Second, mark the buffer unmodified.  This prevents
+            ;; prompts from `kill-buffer' about saving unmodified
+            ;; buffers (when this test is run interactively).
+            ;;
+            ;; Third, delete the temp buffer (by way of
+            ;; `with-temp-buffer' above).  Emacs may also delete the
+            ;; lock file from the test directory at this point.
+            ;;
+            ;; Fourth, delete the test directory itself.
+
+            (set-file-modes temp-dir (logior #o700 (file-modes temp-dir)))
+            (set-buffer-modified-p nil)))
+      (delete-directory temp-dir t nil))))
+
+(defun filelock-tests--make-file-inaccessible (filename)
+  "Make file named FILENAME inaccessible.
+Returns non-nil if the operation succeeds, otherwise nil.  Note:
+not all file systems support this operation."
+  (set-file-modes filename #o000)
+  (equal #o000 (file-modes filename)))
+
+(ert-deftest filelock-tests-lock-unlock-no-errors ()
+  "Check that locking and unlocking works without error."
+  (filelock-tests--fixture
+   (lambda (_)
+     (should (not (file-locked-p (buffer-file-name))))
+     (insert "this locks the buffer's file")
+     (should (file-locked-p (buffer-file-name)))
+     (unlock-buffer)
+     (should (not (file-locked-p (buffer-file-name)))))))
+
+(ert-deftest filelock-tests-lock-buffer-permission-denied ()
+  "Check that locking a buffer in a directory with no write
+permissions does not work."
+  (filelock-tests--fixture
+   (lambda (temp-dir)
+     (should (not (file-locked-p (buffer-file-name))))
+
+     (let ((original-modes (file-modes temp-dir)))
+       (skip-unless (filelock-tests--make-file-inaccessible temp-dir))
+       ;; FIXME: file system errors when locking a file are ignored;
+       ;; should they be?
+       (insert "this locks the current buffer's file")
+       (set-file-modes temp-dir original-modes))
+
+     (should (not (file-locked-p (buffer-file-name)))))))
+
+(ert-deftest filelock-tests-file-locked-p-permission-denied ()
+  "Check that `file-locked-p' fails if the directory is inaccesible."
+  (filelock-tests--fixture
+   (lambda (temp-dir)
+     (skip-unless (filelock-tests--make-file-inaccessible temp-dir))
+
+     (let ((err (should-error (file-locked-p (buffer-file-name)))))
+       (should (equal (cl-subseq err 0 2)
+                      '(file-error "Testing file lock")))))))
+
+(ert-deftest filelock-tests-unlock-permission-denied ()
+  "Check that `unlock-buffer' fails in directories that cannot be
+modified."
+  (filelock-tests--fixture
+   (lambda (temp-dir)
+     (insert "this locks the current buffer's file")
+     (should (file-locked-p (buffer-file-name)))
+
+     (skip-unless (filelock-tests--make-file-inaccessible temp-dir))
+
+     ;; FIXME: Unlocking buffers should not signal errors related to
+     ;; their lock files (bug#46397).
+     (let ((err (should-error (unlock-buffer))))
+       (should (equal (cl-subseq err 0 2)
+                      '(file-error "Unlocking file")))))))
+
+(defun filelock-tests--yes (&rest _)
+  "Return t."
+  t)
+
+(ert-deftest filelock-tests-kill-buffer-permission-denied ()
+  "Check that `unlock-buffer' fails in directories that cannot be
+modified."
+  (filelock-tests--fixture
+   (lambda (temp-dir)
+     (insert "this should lock the buffer")
+     (should (file-locked-p (buffer-file-name)))
+
+     (skip-unless (filelock-tests--make-file-inaccessible temp-dir))
+
+     ;; Kill the current buffer even if it is modified.  Use advice to
+     ;; fake a "yes" answer for the "Buffer modified; kill anyway?"
+     ;; prompt.  Leave the buffer modified so `kill-buffer' will
+     ;; attempt to unlock the buffer's file.
+     ;;
+     ;; FIXME: Killing buffers should not signal errors related to
+     ;; their lock files (bug#46397).
+     (let ((err (unwind-protect
+                    (progn
+                      (advice-add 'yes-or-no-p
+                                  :override
+                                  'filelock-tests--yes)
+                      (should-error (kill-buffer)))
+                  (advice-remove 'yes-or-no-p 'filelock-tests--yes))))
+       (should (equal (cl-subseq err 0 2)
+                      '(file-error "Unlocking file")))))))
+
+(provide 'filelock-tests)
+;;; filelock-tests.el ends here
-- 
2.30.1


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-05 22:19                                         ` Matt Armstrong
@ 2021-03-06  9:36                                           ` Eli Zaretskii
  2021-03-06 23:39                                             ` Matt Armstrong
  2021-03-07  2:50                                             ` Paul Eggert
  0 siblings, 2 replies; 55+ messages in thread
From: Eli Zaretskii @ 2021-03-06  9:36 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
> Date: Fri, 05 Mar 2021 14:19:53 -0800
> 
> > As for the tests you posted: too many of them rely on Posix file
> > modes, and thus will probably either fail or be unable to provide
> > meaningful testing on MS-Windows.  Can we please augment that by tests
> > that create unlocking problems by, e.g., running a shell command to
> > remove or rename or otherwise sabotage the lock file, so that the new
> > functionality could be meaningfully tested on Windows as well?
> 
> I have not been able to come up with a way to achieve what you ask for.

I'm not yet sure I understand why.  I ask several questions about this
below.

> Another issue is that the lock file is typicaly a symbolink link, and
> operating systems typically ignore file modes on symbolic links

On MS-Windows, lock files aren't symlinks, they are regular files.
You should be able to see that in filelock.c.

> so it
> is hard to put the lock file itself into an "invalid" state --
> i.e. where simply attempting to access or delete it generates a file
> system level error.  This is why I resorted to modifying file modes on
> the containing directory -- attemps to remove or access files in such a
> directory do reliably generate errors (on POSIX systems).

I'm asking what is the difference, from the file-locking POV, between
an inaccessible directory and a directory that doesn't exist?  in both
cases, the directory and a lock file inside it are "inaccessible",
right?  So would we be able to make the same or similar tests by
deleting or renaming the directory with the lock file, as we do by
making the directory inaccessible?

> What I've done is make the tests skip themselves when `set-file-mode' on
> the test's temporary directory appears to not work.  When I test this on
> an ext2 file system the tests complete.  When I test this on a FAT and
> NTFS file system (set up as a ramdisk on GNU/Linux), the tests skip
> themselves.

That's exactly the issue: I'd like the tests to not be skipped on
Windows, at least some of them, so that the underlying functionality
could be tested there as well.

> +(defvar filelock-tests-temporary-file-directory nil
> +  "The directory for writing temporary files in filelock tests.
> +This is used by the function
> +`filelock-tests--make-temp-directory' to override the value of
> +the variable `temporary-file-directory'.")

Is this meant to be used to debug the tests?  If so, I suggest to
mention that, and the reason for overriding temporary-file-directory,
in the doc string.

> +(defun filelock-tests--make-temp-directory ()
> +  "Create and return the name of a temporary directory.
> +The caller should delete the directory."
> +  (let ((temporary-file-directory (or filelock-tests-temporary-file-directory
> +                                      temporary-file-directory)))
> +    (make-temp-file "test" t)))

Please use a name that is more indicative of this test, so that the
directory could be more easily spotted.

> +            (set-file-modes temp-dir (logior #o700 (file-modes temp-dir)))
> +            (set-buffer-modified-p nil)))
> +      (delete-directory temp-dir t nil))))

Should these be inside ignore-errors?  Or do you _want_ them to signal
errors if something goes wrong with the underlying file I/O calls?

> +(ert-deftest filelock-tests-lock-unlock-no-errors ()
> +  "Check that locking and unlocking works without error."
> +  (filelock-tests--fixture
> +   (lambda (_)
> +     (should (not (file-locked-p (buffer-file-name))))

ert has 'should-not', why don't you use it (here and elsewhere)?

> +(ert-deftest filelock-tests-lock-buffer-permission-denied ()
> +  "Check that locking a buffer in a directory with no write
> +permissions does not work."
> +  (filelock-tests--fixture
> +   (lambda (temp-dir)
> +     (should (not (file-locked-p (buffer-file-name))))
> +
> +     (let ((original-modes (file-modes temp-dir)))
> +       (skip-unless (filelock-tests--make-file-inaccessible temp-dir))
> +       ;; FIXME: file system errors when locking a file are ignored;
> +       ;; should they be?
> +       (insert "this locks the current buffer's file")
> +       (set-file-modes temp-dir original-modes))
> +
> +     (should (not (file-locked-p (buffer-file-name)))))))

Would this test work if, instead of making the directory inaccessible,
you renamed it to another name (and then renamed back)?  If not, why
not?

> +(ert-deftest filelock-tests-file-locked-p-permission-denied ()
> +  "Check that `file-locked-p' fails if the directory is inaccesible."
> +  (filelock-tests--fixture
> +   (lambda (temp-dir)
> +     (skip-unless (filelock-tests--make-file-inaccessible temp-dir))
> +
> +     (let ((err (should-error (file-locked-p (buffer-file-name)))))
> +       (should (equal (cl-subseq err 0 2)
> +                      '(file-error "Testing file lock")))))))

cl-subseq needs to require cl-extra, AFAICT.

> +     ;; Kill the current buffer even if it is modified.  Use advice to
> +     ;; fake a "yes" answer for the "Buffer modified; kill anyway?"

You don't need to use advice-add, you can simply override the
definition of yes-or-no-p.

Thanks.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-06  9:36                                           ` Eli Zaretskii
@ 2021-03-06 23:39                                             ` Matt Armstrong
  2021-03-07  2:50                                             ` Paul Eggert
  1 sibling, 0 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-03-06 23:39 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Matt Armstrong <matt@rfc20.org>
>> Cc: 46397@debbugs.gnu.org, eggert@cs.ucla.edu, craven@gmx.net
>> Date: Fri, 05 Mar 2021 14:19:53 -0800
>> 
>> > As for the tests you posted: too many of them rely on Posix file
>> > modes, and thus will probably either fail or be unable to provide
>> > meaningful testing on MS-Windows.  Can we please augment that by tests
>> > that create unlocking problems by, e.g., running a shell command to
>> > remove or rename or otherwise sabotage the lock file, so that the new
>> > functionality could be meaningfully tested on Windows as well?
>> 
>> I have not been able to come up with a way to achieve what you ask for.
>
> I'm not yet sure I understand why.  I ask several questions about this
> below.

[...]

> I'm asking what is the difference, from the file-locking POV, between
> an inaccessible directory and a directory that doesn't exist?  in both
> cases, the directory and a lock file inside it are "inaccessible",
> right?  So would we be able to make the same or similar tests by
> deleting or renaming the directory with the lock file, as we do by
> making the directory inaccessible?

This relates to your objection to the handling of ENOTDIR that Paul
added early on in this bug.  If you recall, he changed filelock.c to
treat ENOTDIR equivalently to ENOENT, at least in the code path taken by
`unlock-file'.

If we revert that change, then it again becomes possible to induce an
`unlock-file' error by attempting to unlock a modified buffer whose
`buffer-file-truename' names a non-existent directory.

My guess is that this would be portable to MS-Windows.  The only way it
wouldn't is if the MS-Windows implementation of unlock() returns ENOENT
if the directory along the path does not exist.


>> What I've done is make the tests skip themselves when `set-file-mode' on
>> the test's temporary directory appears to not work.  When I test this on
>> an ext2 file system the tests complete.  When I test this on a FAT and
>> NTFS file system (set up as a ramdisk on GNU/Linux), the tests skip
>> themselves.
>
> That's exactly the issue: I'd like the tests to not be skipped on
> Windows, at least some of them, so that the underlying functionality
> could be tested there as well.

Reverting Paul's ENOTDIR change to the unlock code path would give us a
good option for testing this in a portable way.

I am, given that the plan is to treat all such errors as
`display-warning' events, and the relative rarity of the situation
coming up in practice, it feels like the simplest way to exercise this
kind of error in a portable test.

Paul, would you be okay with that approach?





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-06  9:36                                           ` Eli Zaretskii
  2021-03-06 23:39                                             ` Matt Armstrong
@ 2021-03-07  2:50                                             ` Paul Eggert
  2021-03-07  5:57                                               ` Matt Armstrong
  1 sibling, 1 reply; 55+ messages in thread
From: Paul Eggert @ 2021-03-07  2:50 UTC (permalink / raw)
  To: Eli Zaretskii, Matt Armstrong; +Cc: 46397, craven

On 3/6/21 1:36 AM, Eli Zaretskii wrote:
> I'm asking what is the difference, from the file-locking POV, between
> an inaccessible directory and a directory that doesn't exist?

In the former (EACCES) case, it's possible that the lockfile exists but 
Emacs cannot remove it because the user currently lacks permissions to 
an ancestor directory; this serious problem is worth reporting to the user.

In the latter (ENOTDIR) case the lockfile cannot possibly exist, and 
Emacs can treat ENOTDIR differently from EACCES for that reason. It 
makes sense for Emacs to treat ENOTDIR like ENOENT, since in both cases 
the lockfile cannot possibly exist and these are the only two error 
numbers with that property.

It might also make sense to treat ENOTDIR specially (neither like EACCES 
nor like ENOENT) but that's a discussion we haven't had yet, as far as I 
know.

We've spent more time on this ENOENT/ENOTDIR issue than it's worth, so 
feel free to revert my change and install whatever other change you 
like. I'm sure there are lots of ways to fix the underlying problem.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-07  2:50                                             ` Paul Eggert
@ 2021-03-07  5:57                                               ` Matt Armstrong
  0 siblings, 0 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-03-07  5:57 UTC (permalink / raw)
  To: Paul Eggert, Eli Zaretskii; +Cc: 46397, craven

Paul Eggert <eggert@cs.ucla.edu> writes:

> On 3/6/21 1:36 AM, Eli Zaretskii wrote:
>> I'm asking what is the difference, from the file-locking POV, between
>> an inaccessible directory and a directory that doesn't exist?
>
> In the former (EACCES) case, it's possible that the lockfile exists but 
> Emacs cannot remove it because the user currently lacks permissions to 
> an ancestor directory; this serious problem is worth reporting to the user.
>
> In the latter (ENOTDIR) case the lockfile cannot possibly exist, and 
> Emacs can treat ENOTDIR differently from EACCES for that reason. It 
> makes sense for Emacs to treat ENOTDIR like ENOENT, since in both cases 
> the lockfile cannot possibly exist and these are the only two error 
> numbers with that property.
>
> It might also make sense to treat ENOTDIR specially (neither like EACCES 
> nor like ENOENT) but that's a discussion we haven't had yet, as far as I 
> know.
>
> We've spent more time on this ENOENT/ENOTDIR issue than it's worth, so 
> feel free to revert my change and install whatever other change you 
> like. I'm sure there are lots of ways to fix the underlying problem.

Okay, I'll work on that route, thanks Paul.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-19 19:45                             ` Paul Eggert
  2021-02-19 21:52                               ` Matt Armstrong
@ 2021-03-08  2:18                               ` Matt Armstrong
  2021-03-11 14:34                                 ` Eli Zaretskii
  1 sibling, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-03-08  2:18 UTC (permalink / raw)
  To: Paul Eggert, Eli Zaretskii; +Cc: 46397, craven

[-- Attachment #1: Type: text/plain, Size: 1211 bytes --]

Paul Eggert <eggert@cs.ucla.edu> writes:

> On 2/19/21 11:10 AM, Matt Armstrong wrote:
>>   (a) Modify `kill-buffer' to call `unlock-buffer' sooner, closer to the
>>       point where it is already running hooks prompting the user.  Handle
>>       unlock errors there by prompting.
>
> How would unlock-buffer's API change, so that kill-buffer and 
> unlock-buffer's other callers can do the right thing? Would it signal an 
> error, and if so which one (or a new kind of error)?

It turns out that naming a directory the same as the would-be lock file
suffices to induce errors in a portable way.

I've updated the (substantially simplified) test in patch 0001,
incorporating Eli's feedback.

Patch 0002 implements the handling of unlock errors as warnings by way
of a userlock.el function.  I figure that function would be a handy hook
for people to use with `debug-on-entry', if they want.

Both ready for review now.

Things I noticed:

 a) if a buffer becomes unmodified by way of `undo' it is unlocked
    twice, causing two warnings.
 b) the file errors generated contain the original file name, not the
    lock file name.

I am treating both of of those things as not worth fixing, at least for
now.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-test-coverage-for-src-filelock.c-Bug-46397.patch --]
[-- Type: text/x-diff, Size: 7668 bytes --]

From f2969e568b579e49dbe2f73cbf9961d5f32503aa Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Mon, 15 Feb 2021 12:59:08 -0800
Subject: [PATCH 1/2] Add test coverage for src/filelock.c (Bug#46397)

* test/src/filelock-tests.el: New file.
---
 test/src/filelock-tests.el | 173 +++++++++++++++++++++++++++++++++++++
 1 file changed, 173 insertions(+)
 create mode 100644 test/src/filelock-tests.el

diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
new file mode 100644
index 0000000000..c6f55efd49
--- /dev/null
+++ b/test/src/filelock-tests.el
@@ -0,0 +1,173 @@
+;;; filelock-tests.el --- test file locking -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file tests code in src/filelock.c and, to some extent, the
+;; related code in src/fileio.c.
+;;
+;; See also (info "(emacs)Interlocking") and (info "(elisp)File Locks")
+
+;;; Code:
+
+(require 'cl-macs)
+(require 'ert)
+(require 'seq)
+
+(defun filelock-tests--fixture (test-function)
+  "Call TEST-FUNCTION under a test fixture.
+Create a test directory and a buffer whose `buffer-file-name' and
+`buffer-file-truename' are a file within it, then call
+TEST-FUNCTION.  Finally, delete the buffer and the test
+directory."
+  (let* ((temp-dir (make-temp-file "filelock-tests" t))
+         (name (concat (file-name-as-directory temp-dir)
+                       "userfile"))
+         (create-lockfiles t))
+    (unwind-protect
+        (with-temp-buffer
+          (setq buffer-file-name name
+                buffer-file-truename name)
+          (unwind-protect
+              (save-current-buffer
+                (funcall test-function))
+            ;; Set `buffer-file-truename' nil to prevent unlocking,
+            ;; which might prompt the user and/or signal errors.
+            (setq buffer-file-name nil
+                  buffer-file-truename nil)))
+      (delete-directory temp-dir t nil))))
+
+(defun filelock-tests--make-lock-name (file-name)
+  "Return the lock file name for FILE-NAME.
+Equivalent logic in Emacs proper is implemented in C and
+unavailable to Lisp."
+  (concat (file-name-directory (expand-file-name file-name))
+          ".#"
+          (file-name-nondirectory file-name)))
+
+(defun filelock-tests--spoil-lock-file (file-name)
+  "Spoil the lock file for FILE-NAME.
+Cause Emacs to report errors for various file locking operations
+on FILE-NAME going forward.  Create a file that is incompatible
+with Emacs' file locking protocol, but uses the same name as
+FILE-NAME's lock file.  A directory file is used, which is
+portable in practice."
+  (make-directory (filelock-tests--make-lock-name file-name)))
+
+(defun filelock-tests--unspoil-lock-file (file-name)
+  "Remove the lock file spoiler for FILE-NAME.
+See `filelock-tests--spoil-lock-file'."
+  (delete-directory (filelock-tests--make-lock-name file-name) t))
+
+(defun filelock-tests--should-be-locked ()
+  "Abort the current test if the current buffer is not locked.
+Exception: on systems without lock file support, aborts the
+current test if the current file is locked (which should never
+the case)."
+  (if (eq system-type 'ms-dos)
+      (should-not (file-locked-p buffer-file-truename))
+    (should (file-locked-p buffer-file-truename))))
+
+(ert-deftest filelock-tests-lock-unlock-no-errors ()
+  "Check that locking and unlocking works without error."
+  (filelock-tests--fixture
+   (lambda ()
+     (should-not (file-locked-p (buffer-file-name)))
+
+     ;; inserting text should lock the buffer's file.
+     (insert "this locks the buffer's file")
+     (filelock-tests--should-be-locked)
+     (unlock-buffer)
+     (set-buffer-modified-p nil)
+     (should-not (file-locked-p (buffer-file-name)))
+
+     ;; `set-buffer-modified-p' should lock the buffer's file.
+     (set-buffer-modified-p t)
+     (filelock-tests--should-be-locked)
+     (unlock-buffer)
+     (should-not (file-locked-p (buffer-file-name)))
+
+     (should-not (file-locked-p (buffer-file-name))))))
+
+(ert-deftest filelock-tests-lock-spoiled ()
+  "Check `lock-buffer' ."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+     ;; FIXME: errors when locking a file are ignored; should they be?
+     (set-buffer-modified-p t)
+     (filelock-tests--unspoil-lock-file buffer-file-truename)
+     (should-not (file-locked-p buffer-file-truename)))))
+
+(ert-deftest filelock-tests-file-locked-p-spoiled ()
+  "Check that `file-locked-p' fails if the lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+     (let ((err (should-error (file-locked-p (buffer-file-name)))))
+       (should (equal (seq-subseq err 0 2)
+                      '(file-error "Testing file lock")))))))
+
+(ert-deftest filelock-tests-unlock-spoiled ()
+  "Check that `unlock-buffer' fails if the lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     ;; Set the buffer modified with file locking temporarily
+     ;; disabled.
+     (let ((create-lockfiles nil))
+       (set-buffer-modified-p t))
+     (should-not (file-locked-p buffer-file-truename))
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+
+     ;; FIXME: Unlocking buffers should not signal errors related to
+     ;; their lock files (bug#46397).
+     (let ((err (should-error (unlock-buffer))))
+       (should (equal (cl-subseq err 0 2)
+                      '(file-error "Unlocking file")))))))
+
+(ert-deftest filelock-tests-kill-buffer-spoiled ()
+  "Check that `kill-buffer' fails if a lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     ;; Set the buffer modified with file locking temporarily
+     ;; disabled.
+     (let ((create-lockfiles nil))
+       (set-buffer-modified-p t))
+     (should-not (file-locked-p buffer-file-truename))
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+
+     ;; Kill the current buffer.  Because the buffer is modified Emacs
+     ;; will attempt to unlock it.  Temporarily bind `yes-or-no-p' to
+     ;; a function that fakes a "yes" answer for the "Buffer modified;
+     ;; kill anyway?" prompt.
+     ;;
+     ;; FIXME: Killing buffers should not signal errors related to
+     ;; their lock files (bug#46397).
+     (let* ((err (cl-letf (((symbol-function 'yes-or-no-p)
+                            (lambda (&rest _) t)))
+                   (should-error (kill-buffer)))))
+       (should (equal (seq-subseq err 0 2)
+                      '(file-error "Unlocking file")))))))
+
+(provide 'filelock-tests)
+;;; filelock-tests.el ends here
-- 
2.30.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-File-unlock-errors-now-issue-warnings-Bug-46397.patch --]
[-- Type: text/x-diff, Size: 4625 bytes --]

From 7b272d53d603d9f3cfd2421d31e5891723fc2ca1 Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Fri, 19 Feb 2021 15:39:15 -0800
Subject: [PATCH 2/2] File unlock errors now issue warnings (Bug#46397)

* lisp/userlock.el (userlock--handle-unlock-error): New function, call
`display-error'.
* src/filelock.c (unlock_file_body): New function, do what
unlock_file() used to.
(unlock_file_handle_error): New function, call
`userlock--handle-unlock-error' with the captured error.
(unlock_file): Use condition case to glue together the above.
* test/src/filelock-tests.el (filelock-tests-kill-buffer-spoiled):
(filelock-tests-unlock-spoiled): Modify to test new behavior.
---
 lisp/userlock.el           |  9 +++++++++
 src/filelock.c             | 21 +++++++++++++++++++--
 test/src/filelock-tests.el | 34 ++++++++++++++++++++++------------
 3 files changed, 50 insertions(+), 14 deletions(-)

diff --git a/lisp/userlock.el b/lisp/userlock.el
index a340ff85b2..d33089b11a 100644
--- a/lisp/userlock.el
+++ b/lisp/userlock.el
@@ -193,4 +193,13 @@ ask-user-about-supersession-help
     (with-current-buffer standard-output
       (help-mode))))
 
+;;;###autoload
+(defun userlock--handle-unlock-error (err)
+  "Report an error ERR that occurred while unlocking a file."
+  (display-warning
+   '(unlock-file)
+   (message "Error unlocking file, proceeding as if unlocked: %s"
+            (error-message-string err))
+   :warning))
+
 ;;; userlock.el ends here
diff --git a/src/filelock.c b/src/filelock.c
index 373fc00a42..1f5bba4447 100644
--- a/src/filelock.c
+++ b/src/filelock.c
@@ -719,8 +719,8 @@ lock_file (Lisp_Object fn)
     }
 }
 
-void
-unlock_file (Lisp_Object fn)
+static Lisp_Object
+unlock_file_body (Lisp_Object fn)
 {
   char *lfname;
   USE_SAFE_ALLOCA;
@@ -737,6 +737,23 @@ unlock_file (Lisp_Object fn)
     report_file_errno ("Unlocking file", filename, err);
 
   SAFE_FREE ();
+  return Qnil;
+}
+
+static Lisp_Object
+unlock_file_handle_error (Lisp_Object err)
+{
+  call1 (intern ("userlock--handle-unlock-error"), err);
+  return Qnil;
+}
+
+void
+unlock_file (Lisp_Object fn)
+{
+  internal_condition_case_1 (unlock_file_body,
+			     fn,
+			     list1(Qfile_error),
+			     unlock_file_handle_error);
 }
 
 #else  /* MSDOS */
diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
index c6f55efd49..a96d6d6728 100644
--- a/test/src/filelock-tests.el
+++ b/test/src/filelock-tests.el
@@ -138,11 +138,16 @@ filelock-tests-unlock-spoiled
      (should-not (file-locked-p buffer-file-truename))
      (filelock-tests--spoil-lock-file buffer-file-truename)
 
-     ;; FIXME: Unlocking buffers should not signal errors related to
-     ;; their lock files (bug#46397).
-     (let ((err (should-error (unlock-buffer))))
-       (should (equal (cl-subseq err 0 2)
-                      '(file-error "Unlocking file")))))))
+     ;; Errors from `unlock-buffer' should call
+     ;; `userlock--handle-unlock-error' (bug#46397).
+     (let (errors)
+       (cl-letf (((symbol-function 'userlock--handle-unlock-error)
+                  (lambda (err) (push err errors))))
+         (unlock-buffer))
+       (should (consp errors))
+       (should (equal '(file-error "Unlocking file")
+                      (seq-subseq (car errors) 0 2)))
+       (should (equal (length errors) 1))))))
 
 (ert-deftest filelock-tests-kill-buffer-spoiled ()
   "Check that `kill-buffer' fails if a lockfile is \"spoiled\"."
@@ -161,13 +166,18 @@ filelock-tests-kill-buffer-spoiled
      ;; a function that fakes a "yes" answer for the "Buffer modified;
      ;; kill anyway?" prompt.
      ;;
-     ;; FIXME: Killing buffers should not signal errors related to
-     ;; their lock files (bug#46397).
-     (let* ((err (cl-letf (((symbol-function 'yes-or-no-p)
-                            (lambda (&rest _) t)))
-                   (should-error (kill-buffer)))))
-       (should (equal (seq-subseq err 0 2)
-                      '(file-error "Unlocking file")))))))
+     ;; File errors from unlocking files should call
+     ;; `userlock--handle-unlock-error' (bug#46397).
+     (let (errors)
+       (cl-letf (((symbol-function 'yes-or-no-p)
+                  (lambda (&rest _) t))
+                 ((symbol-function 'userlock--handle-unlock-error)
+                  (lambda (err) (push err errors))))
+         (kill-buffer))
+       (should (consp errors))
+       (should (equal '(file-error "Unlocking file")
+                      (seq-subseq (car errors) 0 2)))
+       (should (equal (length errors) 1))))))
 
 (provide 'filelock-tests)
 ;;; filelock-tests.el ends here
-- 
2.30.1


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-08  2:18                               ` Matt Armstrong
@ 2021-03-11 14:34                                 ` Eli Zaretskii
  2021-03-17 23:49                                   ` Matt Armstrong
  2021-03-17 23:51                                   ` Matt Armstrong
  0 siblings, 2 replies; 55+ messages in thread
From: Eli Zaretskii @ 2021-03-11 14:34 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: 46397@debbugs.gnu.org, craven@gmx.net
> Date: Sun, 07 Mar 2021 18:18:46 -0800
> 
> It turns out that naming a directory the same as the would-be lock file
> suffices to induce errors in a portable way.
> 
> I've updated the (substantially simplified) test in patch 0001,
> incorporating Eli's feedback.
> 
> Patch 0002 implements the handling of unlock errors as warnings by way
> of a userlock.el function.  I figure that function would be a handy hook
> for people to use with `debug-on-entry', if they want.
> 
> Both ready for review now.

Thanks, I have just two minor comments:

  . I'd prefer a slightly different warning text, see below
  . We need this change to be reflected in NEWS and perhaps in the manual

> Things I noticed:
> 
>  a) if a buffer becomes unmodified by way of `undo' it is unlocked
>     twice, causing two warnings.
>  b) the file errors generated contain the original file name, not the
>     lock file name.
> 
> I am treating both of of those things as not worth fixing, at least for
> now.

Right, I agree.

> +(defun userlock--handle-unlock-error (err)
> +  "Report an error ERR that occurred while unlocking a file."
> +  (display-warning
> +   '(unlock-file)
> +   (message "Error unlocking file, proceeding as if unlocked: %s"
> +            (error-message-string err))

I think "proceeding as if unlocked" may be slightly confusing,
especially for non-native English speakers.  How about this instead:

  Error unlocking file %s, ignored

Thanks.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-02-22  1:42                                       ` Matt Armstrong
@ 2021-03-14 18:03                                         ` Bill Wohler
  2021-03-17 23:36                                           ` Matt Armstrong
  0 siblings, 1 reply; 55+ messages in thread
From: Bill Wohler @ 2021-03-14 18:03 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: eggert, 46397, craven, Mike Kupfer

Matt Armstrong <matt@rfc20.org> wrote:

> Mike Kupfer <mkupfer@alum.berkeley.edu> writes:
> 
> > (Adding Bill Wohler, who has a better grasp than I about why MH-E does
> > some things.)
> >
> > Matt Armstrong wrote:
> >
> >> Eli Zaretskii <eliz@gnu.org> writes:
> >> 
> >> > I think we should audit all the callers of unlock_buffer and
> >> > unlock_file, and see if signaling an error there is really the best
> >> > alternative.
> > [...]
> >> 	* lisp/mh-e/mh-show.el (mh-clean-msg-header, mh-unvisit-file):
> >>     	  hard to say, very old code...
> >> 	* lisp/mh-e/mh-comp.el (mh-read-draft): ditto.
> >
> > I'm not sure I completely understanding the logic behind those calls to
> > unlock-buffer, but I'll take a stab at it.
> 
> [...]
> 
> Thanks for those analysis Mike.  They make sense to me.

This code was likely originally written in the 80s and well before my
time in any case, and it isn't code that I've worked with. I concur with
Mike's analysis as well, and thank him for diving in.

[...]

> > I don't understand the proposal for unlock-buffer (or something under
> > it) to prompt the user.  IIUC, the proposal is for a prompt like
> >
> >> /tmp/x/y lock is invalid; assume unlocked? (yes or no)
> >
> > I assume that if the user responds with "yes", unlock-buffer returns
> > and the caller is none the wiser.  If the user responds with "no",
> > what happens?
> >
> > mike
> 
> I think under the current idea, in the case of `kill-buffer', answering
> "no" from the prompt the buffer un-killed.  I think the technical
> mechanism would be to either re-signal the underlying 'file-error or
> signal a new 'unlock-error that contains similar information.

Since I'm reading this out of context, I don't understand it :-). I
think that if an MH-E user, including me, got the prompt that Mike
suggested, she would be pretty confused. If the issue at hand arises, it
would be preferable to speak in the MH-E user's language, such as: Error
recycling draft buffer, discard or keep? [keep].

-- 
Bill Wohler <wohler@newt.com> aka <Bill.Wohler@nasa.gov>
http://www.newt.com/wohler/, GnuPG ID:610BD9AD





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-14 18:03                                         ` Bill Wohler
@ 2021-03-17 23:36                                           ` Matt Armstrong
  0 siblings, 0 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-03-17 23:36 UTC (permalink / raw)
  To: Bill Wohler; +Cc: eggert, 46397, craven, Mike Kupfer

Bill Wohler <wohler@newt.com> writes:

> Matt Armstrong <matt@rfc20.org> wrote:
>
>> Mike Kupfer <mkupfer@alum.berkeley.edu> writes:
>> 
>> > I'm not sure I completely understanding the logic behind those calls to
>> > unlock-buffer, but I'll take a stab at it.
>> 
>> [...]
>> 
>> Thanks for those analysis Mike.  They make sense to me.
>
> This code was likely originally written in the 80s and well before my
> time in any case, and it isn't code that I've worked with. I concur with
> Mike's analysis as well, and thank him for diving in.

Yes, it looked like old code to me as well.  I suspect that the explicit
`unlock-buffer' call could be eliminated by rewriting this code with the
primitives Emacs provides now.  E.g. a call to `(set-buffer-modified-p
nil)' would call `unlock-buffer' implicitly, if necessary, and might
more clearly convey the problem the original author was trying to solve.

In any case, I don't intend to pursue this further.


> Since I'm reading this out of context, I don't understand it :-). I
> think that if an MH-E user, including me, got the prompt that Mike
> suggested, she would be pretty confused. If the issue at hand arises, it
> would be preferable to speak in the MH-E user's language, such as: Error
> recycling draft buffer, discard or keep? [keep].

The currently proposed solution is to demote file level errors from
`unlock-error' to warnings.  I suspect that Emacs had no standard way to
display warnings in the 80s either!





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-11 14:34                                 ` Eli Zaretskii
@ 2021-03-17 23:49                                   ` Matt Armstrong
  2021-03-17 23:51                                   ` Matt Armstrong
  1 sibling, 0 replies; 55+ messages in thread
From: Matt Armstrong @ 2021-03-17 23:49 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

Eli Zaretskii <eliz@gnu.org> writes:

> Thanks, I have just two minor comments:
>
>   . I'd prefer a slightly different warning text, see below
>   . We need this change to be reflected in NEWS and perhaps in the manual

Thank you for the review Eli.  I've incorporated your feedback in the
attached patches.

I made the warning message even more terse than you suggested because
before doing so the warnings looked like this:

    Warning (unlock-file): Error unlocking file Unlocking file:
    Permission denied, /tmp/inaccessible/foo, ignored [Disable showing]
    [Disable logging]

...which says "unlock file" too many times.  With the current patch it
is a little bit better:

    Warning (unlock-file): Unlocking file: Permission denied,
    /tmp/inaccessible/foo, ignored [Disable showing] [Disable logging]

The "Unlocking file: Permission denied, /tmp/inaccessible/foo" is a form
all `file-error' conditions have.  Most relevant here is the "Unlocking
file" prefix, which the C level API that generates these errors makes
difficult to omit.





^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-11 14:34                                 ` Eli Zaretskii
  2021-03-17 23:49                                   ` Matt Armstrong
@ 2021-03-17 23:51                                   ` Matt Armstrong
  2021-03-20 10:43                                     ` Eli Zaretskii
  1 sibling, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-03-17 23:51 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

[-- Attachment #1: Type: text/plain, Size: 1101 bytes --]

Eli Zaretskii <eliz@gnu.org> writes:

> Thanks, I have just two minor comments:
>
>   . I'd prefer a slightly different warning text, see below
>   . We need this change to be reflected in NEWS and perhaps in the manual

(resending with the patches attached this time)

Thank you for the review Eli.  I've incorporated your feedback in the
attached patches.

I made the warning message even more terse than you suggested because
before doing so the warnings looked like this:

    Warning (unlock-file): Error unlocking file Unlocking file:
    Permission denied, /tmp/inaccessible/foo, ignored [Disable showing]
    [Disable logging]

...which says "unlock file" too many times.  With the current patch it
is a little bit better:

    Warning (unlock-file): Unlocking file: Permission denied,
    /tmp/inaccessible/foo, ignored [Disable showing] [Disable logging]

The "Unlocking file: Permission denied, /tmp/inaccessible/foo" is a form
all `file-error' conditions have.  Most relevant here is the "Unlocking
file" prefix, which the C level API that generates these errors makes
difficult to omit.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: v3-0001-Add-test-coverage-for-src-filelock.c-Bug-46397.patch --]
[-- Type: text/x-diff, Size: 7671 bytes --]

From 8511e13a6e4cd1419ae2612feb77c182533054bc Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Mon, 15 Feb 2021 12:59:08 -0800
Subject: [PATCH v3 1/2] Add test coverage for src/filelock.c (Bug#46397)

* test/src/filelock-tests.el: New file.
---
 test/src/filelock-tests.el | 173 +++++++++++++++++++++++++++++++++++++
 1 file changed, 173 insertions(+)
 create mode 100644 test/src/filelock-tests.el

diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
new file mode 100644
index 0000000000..c6f55efd49
--- /dev/null
+++ b/test/src/filelock-tests.el
@@ -0,0 +1,173 @@
+;;; filelock-tests.el --- test file locking -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file tests code in src/filelock.c and, to some extent, the
+;; related code in src/fileio.c.
+;;
+;; See also (info "(emacs)Interlocking") and (info "(elisp)File Locks")
+
+;;; Code:
+
+(require 'cl-macs)
+(require 'ert)
+(require 'seq)
+
+(defun filelock-tests--fixture (test-function)
+  "Call TEST-FUNCTION under a test fixture.
+Create a test directory and a buffer whose `buffer-file-name' and
+`buffer-file-truename' are a file within it, then call
+TEST-FUNCTION.  Finally, delete the buffer and the test
+directory."
+  (let* ((temp-dir (make-temp-file "filelock-tests" t))
+         (name (concat (file-name-as-directory temp-dir)
+                       "userfile"))
+         (create-lockfiles t))
+    (unwind-protect
+        (with-temp-buffer
+          (setq buffer-file-name name
+                buffer-file-truename name)
+          (unwind-protect
+              (save-current-buffer
+                (funcall test-function))
+            ;; Set `buffer-file-truename' nil to prevent unlocking,
+            ;; which might prompt the user and/or signal errors.
+            (setq buffer-file-name nil
+                  buffer-file-truename nil)))
+      (delete-directory temp-dir t nil))))
+
+(defun filelock-tests--make-lock-name (file-name)
+  "Return the lock file name for FILE-NAME.
+Equivalent logic in Emacs proper is implemented in C and
+unavailable to Lisp."
+  (concat (file-name-directory (expand-file-name file-name))
+          ".#"
+          (file-name-nondirectory file-name)))
+
+(defun filelock-tests--spoil-lock-file (file-name)
+  "Spoil the lock file for FILE-NAME.
+Cause Emacs to report errors for various file locking operations
+on FILE-NAME going forward.  Create a file that is incompatible
+with Emacs' file locking protocol, but uses the same name as
+FILE-NAME's lock file.  A directory file is used, which is
+portable in practice."
+  (make-directory (filelock-tests--make-lock-name file-name)))
+
+(defun filelock-tests--unspoil-lock-file (file-name)
+  "Remove the lock file spoiler for FILE-NAME.
+See `filelock-tests--spoil-lock-file'."
+  (delete-directory (filelock-tests--make-lock-name file-name) t))
+
+(defun filelock-tests--should-be-locked ()
+  "Abort the current test if the current buffer is not locked.
+Exception: on systems without lock file support, aborts the
+current test if the current file is locked (which should never
+the case)."
+  (if (eq system-type 'ms-dos)
+      (should-not (file-locked-p buffer-file-truename))
+    (should (file-locked-p buffer-file-truename))))
+
+(ert-deftest filelock-tests-lock-unlock-no-errors ()
+  "Check that locking and unlocking works without error."
+  (filelock-tests--fixture
+   (lambda ()
+     (should-not (file-locked-p (buffer-file-name)))
+
+     ;; inserting text should lock the buffer's file.
+     (insert "this locks the buffer's file")
+     (filelock-tests--should-be-locked)
+     (unlock-buffer)
+     (set-buffer-modified-p nil)
+     (should-not (file-locked-p (buffer-file-name)))
+
+     ;; `set-buffer-modified-p' should lock the buffer's file.
+     (set-buffer-modified-p t)
+     (filelock-tests--should-be-locked)
+     (unlock-buffer)
+     (should-not (file-locked-p (buffer-file-name)))
+
+     (should-not (file-locked-p (buffer-file-name))))))
+
+(ert-deftest filelock-tests-lock-spoiled ()
+  "Check `lock-buffer' ."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+     ;; FIXME: errors when locking a file are ignored; should they be?
+     (set-buffer-modified-p t)
+     (filelock-tests--unspoil-lock-file buffer-file-truename)
+     (should-not (file-locked-p buffer-file-truename)))))
+
+(ert-deftest filelock-tests-file-locked-p-spoiled ()
+  "Check that `file-locked-p' fails if the lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+     (let ((err (should-error (file-locked-p (buffer-file-name)))))
+       (should (equal (seq-subseq err 0 2)
+                      '(file-error "Testing file lock")))))))
+
+(ert-deftest filelock-tests-unlock-spoiled ()
+  "Check that `unlock-buffer' fails if the lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     ;; Set the buffer modified with file locking temporarily
+     ;; disabled.
+     (let ((create-lockfiles nil))
+       (set-buffer-modified-p t))
+     (should-not (file-locked-p buffer-file-truename))
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+
+     ;; FIXME: Unlocking buffers should not signal errors related to
+     ;; their lock files (bug#46397).
+     (let ((err (should-error (unlock-buffer))))
+       (should (equal (cl-subseq err 0 2)
+                      '(file-error "Unlocking file")))))))
+
+(ert-deftest filelock-tests-kill-buffer-spoiled ()
+  "Check that `kill-buffer' fails if a lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     ;; Set the buffer modified with file locking temporarily
+     ;; disabled.
+     (let ((create-lockfiles nil))
+       (set-buffer-modified-p t))
+     (should-not (file-locked-p buffer-file-truename))
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+
+     ;; Kill the current buffer.  Because the buffer is modified Emacs
+     ;; will attempt to unlock it.  Temporarily bind `yes-or-no-p' to
+     ;; a function that fakes a "yes" answer for the "Buffer modified;
+     ;; kill anyway?" prompt.
+     ;;
+     ;; FIXME: Killing buffers should not signal errors related to
+     ;; their lock files (bug#46397).
+     (let* ((err (cl-letf (((symbol-function 'yes-or-no-p)
+                            (lambda (&rest _) t)))
+                   (should-error (kill-buffer)))))
+       (should (equal (seq-subseq err 0 2)
+                      '(file-error "Unlocking file")))))))
+
+(provide 'filelock-tests)
+;;; filelock-tests.el ends here
-- 
2.30.2


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: v3-0002-File-unlock-errors-now-issue-warnings-Bug-46397.patch --]
[-- Type: text/x-diff, Size: 6566 bytes --]

From 15a9c0ceba95375a0683de2947c3302c5f676a28 Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Fri, 19 Feb 2021 15:39:15 -0800
Subject: [PATCH v3 2/2] File unlock errors now issue warnings (Bug#46397)

The primary idea is to allow `kill-buffer' and `kill-emacs' to
complete even if Emacs has trouble unlocking the buffer's file.

* lisp/userlock.el (userlock--handle-unlock-error): New function, call
`display-error'.
* src/filelock.c (unlock_file_body): New function, do what
unlock_file() used to.
(unlock_file_handle_error): New function, call
`userlock--handle-unlock-error' with the captured error.
(unlock_file): Handle `file-error' conditions by calling the handler
defined above.
* test/src/filelock-tests.el (filelock-tests-kill-buffer-spoiled):
(filelock-tests-unlock-spoiled): Modify to test new behavior.
---
 doc/lispref/files.texi     |  2 ++
 etc/NEWS                   |  6 ++++++
 lisp/userlock.el           | 10 ++++++++++
 src/filelock.c             | 26 +++++++++++++++++++++++---
 test/src/filelock-tests.el | 34 ++++++++++++++++++++++------------
 5 files changed, 63 insertions(+), 15 deletions(-)

diff --git a/doc/lispref/files.texi b/doc/lispref/files.texi
index 2828b50cad..df7981174a 100644
--- a/doc/lispref/files.texi
+++ b/doc/lispref/files.texi
@@ -764,6 +764,8 @@ File Locks
 if the buffer is modified.  If the buffer is not modified, then
 the file should not be locked, so this function does nothing.  It also
 does nothing if the current buffer is not visiting a file, or is not locked.
+Handles file system errors by calling @code{display-warning} and continuing
+as if the error did not occur.
 @end defun
 
 @defopt create-lockfiles
diff --git a/etc/NEWS b/etc/NEWS
index 6fe98dbc12..e7f1570269 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2449,6 +2449,12 @@ back in Emacs 23.1.  The affected functions are: 'make-obsolete',
 \f
 * Lisp Changes in Emacs 28.1
 
++++
+** 'unlock-buffer' displays warnings instead of signaling.
+Instead of signaling 'file-error' conditions for file system level
+errors, the function now calls 'display-warning' and continues as if
+the error did not occur.
+
 +++
 ** New function 'always'.
 This is identical to 'ignore', but returns t instead.
diff --git a/lisp/userlock.el b/lisp/userlock.el
index 57311ac99c..1ea8825ab7 100644
--- a/lisp/userlock.el
+++ b/lisp/userlock.el
@@ -224,4 +224,14 @@ ask-user-about-supersession-help
           revert-buffer-binding))
         (help-mode)))))
 
+;;;###autoload
+(defun userlock--handle-unlock-error (err)
+  "Report an error ERR that occurred while unlocking a file."
+  (display-warning
+   '(unlock-file)
+   ;; There is no need to explain that this is an unlock error because
+   ;; ERR is a `file-error' condition, which explains this.
+   (message "%s, ignored" (error-message-string err))
+   :warning))
+
 ;;; userlock.el ends here
diff --git a/src/filelock.c b/src/filelock.c
index 373fc00a42..446a262a1c 100644
--- a/src/filelock.c
+++ b/src/filelock.c
@@ -719,8 +719,8 @@ lock_file (Lisp_Object fn)
     }
 }
 
-void
-unlock_file (Lisp_Object fn)
+static Lisp_Object
+unlock_file_body (Lisp_Object fn)
 {
   char *lfname;
   USE_SAFE_ALLOCA;
@@ -737,6 +737,23 @@ unlock_file (Lisp_Object fn)
     report_file_errno ("Unlocking file", filename, err);
 
   SAFE_FREE ();
+  return Qnil;
+}
+
+static Lisp_Object
+unlock_file_handle_error (Lisp_Object err)
+{
+  call1 (intern ("userlock--handle-unlock-error"), err);
+  return Qnil;
+}
+
+void
+unlock_file (Lisp_Object fn)
+{
+  internal_condition_case_1 (unlock_file_body,
+			     fn,
+			     list1(Qfile_error),
+			     unlock_file_handle_error);
 }
 
 #else  /* MSDOS */
@@ -790,7 +807,10 @@ DEFUN ("unlock-buffer", Funlock_buffer, Sunlock_buffer,
        0, 0, 0,
        doc: /* Unlock the file visited in the current buffer.
 If the buffer is not modified, this does nothing because the file
-should not be locked in that case.  */)
+should not be locked in that case.  It also does nothing if the
+current buffer is not visiting a file, or is not locked.  Handles file
+system errors by calling `display-warning' and continuing as if the
+error did not occur.  */)
   (void)
 {
   if (SAVE_MODIFF < MODIFF
diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
index c6f55efd49..a96d6d6728 100644
--- a/test/src/filelock-tests.el
+++ b/test/src/filelock-tests.el
@@ -138,11 +138,16 @@ filelock-tests-unlock-spoiled
      (should-not (file-locked-p buffer-file-truename))
      (filelock-tests--spoil-lock-file buffer-file-truename)
 
-     ;; FIXME: Unlocking buffers should not signal errors related to
-     ;; their lock files (bug#46397).
-     (let ((err (should-error (unlock-buffer))))
-       (should (equal (cl-subseq err 0 2)
-                      '(file-error "Unlocking file")))))))
+     ;; Errors from `unlock-buffer' should call
+     ;; `userlock--handle-unlock-error' (bug#46397).
+     (let (errors)
+       (cl-letf (((symbol-function 'userlock--handle-unlock-error)
+                  (lambda (err) (push err errors))))
+         (unlock-buffer))
+       (should (consp errors))
+       (should (equal '(file-error "Unlocking file")
+                      (seq-subseq (car errors) 0 2)))
+       (should (equal (length errors) 1))))))
 
 (ert-deftest filelock-tests-kill-buffer-spoiled ()
   "Check that `kill-buffer' fails if a lockfile is \"spoiled\"."
@@ -161,13 +166,18 @@ filelock-tests-kill-buffer-spoiled
      ;; a function that fakes a "yes" answer for the "Buffer modified;
      ;; kill anyway?" prompt.
      ;;
-     ;; FIXME: Killing buffers should not signal errors related to
-     ;; their lock files (bug#46397).
-     (let* ((err (cl-letf (((symbol-function 'yes-or-no-p)
-                            (lambda (&rest _) t)))
-                   (should-error (kill-buffer)))))
-       (should (equal (seq-subseq err 0 2)
-                      '(file-error "Unlocking file")))))))
+     ;; File errors from unlocking files should call
+     ;; `userlock--handle-unlock-error' (bug#46397).
+     (let (errors)
+       (cl-letf (((symbol-function 'yes-or-no-p)
+                  (lambda (&rest _) t))
+                 ((symbol-function 'userlock--handle-unlock-error)
+                  (lambda (err) (push err errors))))
+         (kill-buffer))
+       (should (consp errors))
+       (should (equal '(file-error "Unlocking file")
+                      (seq-subseq (car errors) 0 2)))
+       (should (equal (length errors) 1))))))
 
 (provide 'filelock-tests)
 ;;; filelock-tests.el ends here
-- 
2.30.2


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-17 23:51                                   ` Matt Armstrong
@ 2021-03-20 10:43                                     ` Eli Zaretskii
  2021-03-22  1:43                                       ` Matt Armstrong
  0 siblings, 1 reply; 55+ messages in thread
From: Eli Zaretskii @ 2021-03-20 10:43 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: eggert@cs.ucla.edu, 46397@debbugs.gnu.org, craven@gmx.net
> Date: Wed, 17 Mar 2021 16:51:20 -0700
> 
> Thank you for the review Eli.  I've incorporated your feedback in the
> attached patches.

Thanks, we are close.

> I made the warning message even more terse than you suggested because
> before doing so the warnings looked like this:
> 
>     Warning (unlock-file): Error unlocking file Unlocking file:
>     Permission denied, /tmp/inaccessible/foo, ignored [Disable showing]
>     [Disable logging]
> 
> ...which says "unlock file" too many times.  With the current patch it
> is a little bit better:
> 
>     Warning (unlock-file): Unlocking file: Permission denied,
>     /tmp/inaccessible/foo, ignored [Disable showing] [Disable logging]

Fine with me.

> * src/filelock.c (unlock_file_body): New function, do what
> unlock_file() used to.

Please don't use "foo()" to refer to a function 'foo': that looks like
a call of 'foo' with no arguments, which is not what you want.  Our
convention is to use 'foo' instead, quoted 'like this'.

> --- a/doc/lispref/files.texi
> +++ b/doc/lispref/files.texi
> @@ -764,6 +764,8 @@ File Locks
>  if the buffer is modified.  If the buffer is not modified, then
>  the file should not be locked, so this function does nothing.  It also
>  does nothing if the current buffer is not visiting a file, or is not locked.
> +Handles file system errors by calling @code{display-warning} and continuing
   ^^^^^^^
"This function handles ..."

> +** 'unlock-buffer' displays warnings instead of signaling.
> +Instead of signaling 'file-error' conditions for file system level
> +errors, the function now calls 'display-warning' and continues as if
> +the error did not occur.                         ^^^^^^^^^^^^^^^^^^^
   ^^^^^^^^^^^^^^^^^^^^^^^
I'd rephrase "... and otherwise ignores the error".

> +(defun userlock--handle-unlock-error (err)
> +  "Report an error ERR that occurred while unlocking a file."

This will sound better if you rename the argument:

  (defun userlock--handle-unlock-error (error)
    "Report an ERROR that occurred while unlocking a file."






^ permalink raw reply	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-20 10:43                                     ` Eli Zaretskii
@ 2021-03-22  1:43                                       ` Matt Armstrong
  2021-03-27  9:20                                         ` Eli Zaretskii
  0 siblings, 1 reply; 55+ messages in thread
From: Matt Armstrong @ 2021-03-22  1:43 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 46397, eggert, craven

[-- Attachment #1: Type: text/plain, Size: 2432 bytes --]

Eli Zaretskii <eliz@gnu.org> writes:

>> From: Matt Armstrong <matt@rfc20.org>
>> Cc: eggert@cs.ucla.edu, 46397@debbugs.gnu.org, craven@gmx.net
>> Date: Wed, 17 Mar 2021 16:51:20 -0700
>> 
>> Thank you for the review Eli.  I've incorporated your feedback in the
>> attached patches.
>
> Thanks, we are close.

You are welcome.  I've quoted your latest feedback below for reference,
and attached the resulting patches after incorporating the changes.  I
also re-ran the new tests.


>> I made the warning message even more terse than you suggested because
>> before doing so the warnings looked like this:
>> 
>>     Warning (unlock-file): Error unlocking file Unlocking file:
>>     Permission denied, /tmp/inaccessible/foo, ignored [Disable showing]
>>     [Disable logging]
>> 
>> ...which says "unlock file" too many times.  With the current patch it
>> is a little bit better:
>> 
>>     Warning (unlock-file): Unlocking file: Permission denied,
>>     /tmp/inaccessible/foo, ignored [Disable showing] [Disable logging]
>
> Fine with me.
>
>> * src/filelock.c (unlock_file_body): New function, do what
>> unlock_file() used to.
>
> Please don't use "foo()" to refer to a function 'foo': that looks like
> a call of 'foo' with no arguments, which is not what you want.  Our
> convention is to use 'foo' instead, quoted 'like this'.
>
>> --- a/doc/lispref/files.texi
>> +++ b/doc/lispref/files.texi
>> @@ -764,6 +764,8 @@ File Locks
>>  if the buffer is modified.  If the buffer is not modified, then
>>  the file should not be locked, so this function does nothing.  It also
>>  does nothing if the current buffer is not visiting a file, or is not locked.
>> +Handles file system errors by calling @code{display-warning} and continuing
>    ^^^^^^^
> "This function handles ..."
>
>> +** 'unlock-buffer' displays warnings instead of signaling.
>> +Instead of signaling 'file-error' conditions for file system level
>> +errors, the function now calls 'display-warning' and continues as if
>> +the error did not occur.                         ^^^^^^^^^^^^^^^^^^^
>    ^^^^^^^^^^^^^^^^^^^^^^^
> I'd rephrase "... and otherwise ignores the error".
>
>> +(defun userlock--handle-unlock-error (err)
>> +  "Report an error ERR that occurred while unlocking a file."
>
> This will sound better if you rename the argument:
>
>   (defun userlock--handle-unlock-error (error)
>     "Report an ERROR that occurred while unlocking a file."



[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: v4-0001-Add-test-coverage-for-src-filelock.c-Bug-46397.patch --]
[-- Type: text/x-diff, Size: 7671 bytes --]

From f3e82c23aa0290dec8d31f465d4a62704c98d0c1 Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Mon, 15 Feb 2021 12:59:08 -0800
Subject: [PATCH v4 1/2] Add test coverage for src/filelock.c (Bug#46397)

* test/src/filelock-tests.el: New file.
---
 test/src/filelock-tests.el | 173 +++++++++++++++++++++++++++++++++++++
 1 file changed, 173 insertions(+)
 create mode 100644 test/src/filelock-tests.el

diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
new file mode 100644
index 0000000000..c6f55efd49
--- /dev/null
+++ b/test/src/filelock-tests.el
@@ -0,0 +1,173 @@
+;;; filelock-tests.el --- test file locking -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file tests code in src/filelock.c and, to some extent, the
+;; related code in src/fileio.c.
+;;
+;; See also (info "(emacs)Interlocking") and (info "(elisp)File Locks")
+
+;;; Code:
+
+(require 'cl-macs)
+(require 'ert)
+(require 'seq)
+
+(defun filelock-tests--fixture (test-function)
+  "Call TEST-FUNCTION under a test fixture.
+Create a test directory and a buffer whose `buffer-file-name' and
+`buffer-file-truename' are a file within it, then call
+TEST-FUNCTION.  Finally, delete the buffer and the test
+directory."
+  (let* ((temp-dir (make-temp-file "filelock-tests" t))
+         (name (concat (file-name-as-directory temp-dir)
+                       "userfile"))
+         (create-lockfiles t))
+    (unwind-protect
+        (with-temp-buffer
+          (setq buffer-file-name name
+                buffer-file-truename name)
+          (unwind-protect
+              (save-current-buffer
+                (funcall test-function))
+            ;; Set `buffer-file-truename' nil to prevent unlocking,
+            ;; which might prompt the user and/or signal errors.
+            (setq buffer-file-name nil
+                  buffer-file-truename nil)))
+      (delete-directory temp-dir t nil))))
+
+(defun filelock-tests--make-lock-name (file-name)
+  "Return the lock file name for FILE-NAME.
+Equivalent logic in Emacs proper is implemented in C and
+unavailable to Lisp."
+  (concat (file-name-directory (expand-file-name file-name))
+          ".#"
+          (file-name-nondirectory file-name)))
+
+(defun filelock-tests--spoil-lock-file (file-name)
+  "Spoil the lock file for FILE-NAME.
+Cause Emacs to report errors for various file locking operations
+on FILE-NAME going forward.  Create a file that is incompatible
+with Emacs' file locking protocol, but uses the same name as
+FILE-NAME's lock file.  A directory file is used, which is
+portable in practice."
+  (make-directory (filelock-tests--make-lock-name file-name)))
+
+(defun filelock-tests--unspoil-lock-file (file-name)
+  "Remove the lock file spoiler for FILE-NAME.
+See `filelock-tests--spoil-lock-file'."
+  (delete-directory (filelock-tests--make-lock-name file-name) t))
+
+(defun filelock-tests--should-be-locked ()
+  "Abort the current test if the current buffer is not locked.
+Exception: on systems without lock file support, aborts the
+current test if the current file is locked (which should never
+the case)."
+  (if (eq system-type 'ms-dos)
+      (should-not (file-locked-p buffer-file-truename))
+    (should (file-locked-p buffer-file-truename))))
+
+(ert-deftest filelock-tests-lock-unlock-no-errors ()
+  "Check that locking and unlocking works without error."
+  (filelock-tests--fixture
+   (lambda ()
+     (should-not (file-locked-p (buffer-file-name)))
+
+     ;; inserting text should lock the buffer's file.
+     (insert "this locks the buffer's file")
+     (filelock-tests--should-be-locked)
+     (unlock-buffer)
+     (set-buffer-modified-p nil)
+     (should-not (file-locked-p (buffer-file-name)))
+
+     ;; `set-buffer-modified-p' should lock the buffer's file.
+     (set-buffer-modified-p t)
+     (filelock-tests--should-be-locked)
+     (unlock-buffer)
+     (should-not (file-locked-p (buffer-file-name)))
+
+     (should-not (file-locked-p (buffer-file-name))))))
+
+(ert-deftest filelock-tests-lock-spoiled ()
+  "Check `lock-buffer' ."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+     ;; FIXME: errors when locking a file are ignored; should they be?
+     (set-buffer-modified-p t)
+     (filelock-tests--unspoil-lock-file buffer-file-truename)
+     (should-not (file-locked-p buffer-file-truename)))))
+
+(ert-deftest filelock-tests-file-locked-p-spoiled ()
+  "Check that `file-locked-p' fails if the lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+     (let ((err (should-error (file-locked-p (buffer-file-name)))))
+       (should (equal (seq-subseq err 0 2)
+                      '(file-error "Testing file lock")))))))
+
+(ert-deftest filelock-tests-unlock-spoiled ()
+  "Check that `unlock-buffer' fails if the lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     ;; Set the buffer modified with file locking temporarily
+     ;; disabled.
+     (let ((create-lockfiles nil))
+       (set-buffer-modified-p t))
+     (should-not (file-locked-p buffer-file-truename))
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+
+     ;; FIXME: Unlocking buffers should not signal errors related to
+     ;; their lock files (bug#46397).
+     (let ((err (should-error (unlock-buffer))))
+       (should (equal (cl-subseq err 0 2)
+                      '(file-error "Unlocking file")))))))
+
+(ert-deftest filelock-tests-kill-buffer-spoiled ()
+  "Check that `kill-buffer' fails if a lockfile is \"spoiled\"."
+  (skip-unless (not (eq system-type 'ms-dos))) ; no filelock support
+  (filelock-tests--fixture
+   (lambda ()
+     ;; Set the buffer modified with file locking temporarily
+     ;; disabled.
+     (let ((create-lockfiles nil))
+       (set-buffer-modified-p t))
+     (should-not (file-locked-p buffer-file-truename))
+     (filelock-tests--spoil-lock-file buffer-file-truename)
+
+     ;; Kill the current buffer.  Because the buffer is modified Emacs
+     ;; will attempt to unlock it.  Temporarily bind `yes-or-no-p' to
+     ;; a function that fakes a "yes" answer for the "Buffer modified;
+     ;; kill anyway?" prompt.
+     ;;
+     ;; FIXME: Killing buffers should not signal errors related to
+     ;; their lock files (bug#46397).
+     (let* ((err (cl-letf (((symbol-function 'yes-or-no-p)
+                            (lambda (&rest _) t)))
+                   (should-error (kill-buffer)))))
+       (should (equal (seq-subseq err 0 2)
+                      '(file-error "Unlocking file")))))))
+
+(provide 'filelock-tests)
+;;; filelock-tests.el ends here
-- 
2.30.2


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: v4-0002-File-unlock-errors-now-issue-warnings-Bug-46397.patch --]
[-- Type: text/x-diff, Size: 6567 bytes --]

From 08516cc2555a9bd49440ffd3b91b6b294769243a Mon Sep 17 00:00:00 2001
From: Matt Armstrong <matt@rfc20.org>
Date: Fri, 19 Feb 2021 15:39:15 -0800
Subject: [PATCH v4 2/2] File unlock errors now issue warnings (Bug#46397)

The primary idea is to allow `kill-buffer' and `kill-emacs' to
complete even if Emacs has trouble unlocking the buffer's file.

* lisp/userlock.el (userlock--handle-unlock-error): New function, call
`display-error'.
* src/filelock.c (unlock_file_body): New function, do what
'unlock_file' used to.
(unlock_file_handle_error): New function, call
`userlock--handle-unlock-error' with the captured error.
(unlock_file): Handle `file-error' conditions by calling the handler
defined above.
* test/src/filelock-tests.el (filelock-tests-kill-buffer-spoiled):
(filelock-tests-unlock-spoiled): Modify to test new behavior.
---
 doc/lispref/files.texi     |  2 ++
 etc/NEWS                   |  6 ++++++
 lisp/userlock.el           | 10 ++++++++++
 src/filelock.c             | 26 +++++++++++++++++++++++---
 test/src/filelock-tests.el | 34 ++++++++++++++++++++++------------
 5 files changed, 63 insertions(+), 15 deletions(-)

diff --git a/doc/lispref/files.texi b/doc/lispref/files.texi
index 2828b50cad..a8b921eb9f 100644
--- a/doc/lispref/files.texi
+++ b/doc/lispref/files.texi
@@ -764,6 +764,8 @@ File Locks
 if the buffer is modified.  If the buffer is not modified, then
 the file should not be locked, so this function does nothing.  It also
 does nothing if the current buffer is not visiting a file, or is not locked.
+This function handles file system errors by calling @code{display-warning}
+and otherwise ignores the error.
 @end defun
 
 @defopt create-lockfiles
diff --git a/etc/NEWS b/etc/NEWS
index c602166397..232241217a 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2490,6 +2490,12 @@ back in Emacs 23.1.  The affected functions are: 'make-obsolete',
 \f
 * Lisp Changes in Emacs 28.1
 
++++
+** 'unlock-buffer' displays warnings instead of signaling.
+Instead of signaling 'file-error' conditions for file system level
+errors, the function now calls 'display-warning' and continues as if
+the error did not occur.
+
 +++
 ** New function 'always'.
 This is identical to 'ignore', but returns t instead.
diff --git a/lisp/userlock.el b/lisp/userlock.el
index 57311ac99c..4a75815318 100644
--- a/lisp/userlock.el
+++ b/lisp/userlock.el
@@ -224,4 +224,14 @@ ask-user-about-supersession-help
           revert-buffer-binding))
         (help-mode)))))
 
+;;;###autoload
+(defun userlock--handle-unlock-error (error)
+  "Report an ERROR that occurred while unlocking a file."
+  (display-warning
+   '(unlock-file)
+   ;; There is no need to explain that this is an unlock error because
+   ;; ERR is a `file-error' condition, which explains this.
+   (message "%s, ignored" (error-message-string error))
+   :warning))
+
 ;;; userlock.el ends here
diff --git a/src/filelock.c b/src/filelock.c
index 373fc00a42..446a262a1c 100644
--- a/src/filelock.c
+++ b/src/filelock.c
@@ -719,8 +719,8 @@ lock_file (Lisp_Object fn)
     }
 }
 
-void
-unlock_file (Lisp_Object fn)
+static Lisp_Object
+unlock_file_body (Lisp_Object fn)
 {
   char *lfname;
   USE_SAFE_ALLOCA;
@@ -737,6 +737,23 @@ unlock_file (Lisp_Object fn)
     report_file_errno ("Unlocking file", filename, err);
 
   SAFE_FREE ();
+  return Qnil;
+}
+
+static Lisp_Object
+unlock_file_handle_error (Lisp_Object err)
+{
+  call1 (intern ("userlock--handle-unlock-error"), err);
+  return Qnil;
+}
+
+void
+unlock_file (Lisp_Object fn)
+{
+  internal_condition_case_1 (unlock_file_body,
+			     fn,
+			     list1(Qfile_error),
+			     unlock_file_handle_error);
 }
 
 #else  /* MSDOS */
@@ -790,7 +807,10 @@ DEFUN ("unlock-buffer", Funlock_buffer, Sunlock_buffer,
        0, 0, 0,
        doc: /* Unlock the file visited in the current buffer.
 If the buffer is not modified, this does nothing because the file
-should not be locked in that case.  */)
+should not be locked in that case.  It also does nothing if the
+current buffer is not visiting a file, or is not locked.  Handles file
+system errors by calling `display-warning' and continuing as if the
+error did not occur.  */)
   (void)
 {
   if (SAVE_MODIFF < MODIFF
diff --git a/test/src/filelock-tests.el b/test/src/filelock-tests.el
index c6f55efd49..a96d6d6728 100644
--- a/test/src/filelock-tests.el
+++ b/test/src/filelock-tests.el
@@ -138,11 +138,16 @@ filelock-tests-unlock-spoiled
      (should-not (file-locked-p buffer-file-truename))
      (filelock-tests--spoil-lock-file buffer-file-truename)
 
-     ;; FIXME: Unlocking buffers should not signal errors related to
-     ;; their lock files (bug#46397).
-     (let ((err (should-error (unlock-buffer))))
-       (should (equal (cl-subseq err 0 2)
-                      '(file-error "Unlocking file")))))))
+     ;; Errors from `unlock-buffer' should call
+     ;; `userlock--handle-unlock-error' (bug#46397).
+     (let (errors)
+       (cl-letf (((symbol-function 'userlock--handle-unlock-error)
+                  (lambda (err) (push err errors))))
+         (unlock-buffer))
+       (should (consp errors))
+       (should (equal '(file-error "Unlocking file")
+                      (seq-subseq (car errors) 0 2)))
+       (should (equal (length errors) 1))))))
 
 (ert-deftest filelock-tests-kill-buffer-spoiled ()
   "Check that `kill-buffer' fails if a lockfile is \"spoiled\"."
@@ -161,13 +166,18 @@ filelock-tests-kill-buffer-spoiled
      ;; a function that fakes a "yes" answer for the "Buffer modified;
      ;; kill anyway?" prompt.
      ;;
-     ;; FIXME: Killing buffers should not signal errors related to
-     ;; their lock files (bug#46397).
-     (let* ((err (cl-letf (((symbol-function 'yes-or-no-p)
-                            (lambda (&rest _) t)))
-                   (should-error (kill-buffer)))))
-       (should (equal (seq-subseq err 0 2)
-                      '(file-error "Unlocking file")))))))
+     ;; File errors from unlocking files should call
+     ;; `userlock--handle-unlock-error' (bug#46397).
+     (let (errors)
+       (cl-letf (((symbol-function 'yes-or-no-p)
+                  (lambda (&rest _) t))
+                 ((symbol-function 'userlock--handle-unlock-error)
+                  (lambda (err) (push err errors))))
+         (kill-buffer))
+       (should (consp errors))
+       (should (equal '(file-error "Unlocking file")
+                      (seq-subseq (car errors) 0 2)))
+       (should (equal (length errors) 1))))))
 
 (provide 'filelock-tests)
 ;;; filelock-tests.el ends here
-- 
2.30.2


^ permalink raw reply related	[flat|nested] 55+ messages in thread

* bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file
  2021-03-22  1:43                                       ` Matt Armstrong
@ 2021-03-27  9:20                                         ` Eli Zaretskii
  0 siblings, 0 replies; 55+ messages in thread
From: Eli Zaretskii @ 2021-03-27  9:20 UTC (permalink / raw)
  To: Matt Armstrong; +Cc: 46397-done, eggert, craven

> From: Matt Armstrong <matt@rfc20.org>
> Cc: eggert@cs.ucla.edu, 46397@debbugs.gnu.org, craven@gmx.net
> Date: Sun, 21 Mar 2021 18:43:45 -0700
> 
> >> Thank you for the review Eli.  I've incorporated your feedback in the
> >> attached patches.
> >
> > Thanks, we are close.
> 
> You are welcome.  I've quoted your latest feedback below for reference,
> and attached the resulting patches after incorporating the changes.  I
> also re-ran the new tests.

Thanks, I installed these changes, and I'm closing the bug report.





^ permalink raw reply	[flat|nested] 55+ messages in thread

end of thread, other threads:[~2021-03-27  9:20 UTC | newest]

Thread overview: 55+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-02-09  9:47 bug#46397: 27.1; Cannot delete buffer pointing to a file in a path that includes a file Peter
2021-02-09 23:47 ` Matt Armstrong
2021-02-10  0:23   ` Matt Armstrong
2021-02-10 15:05     ` Eli Zaretskii
2021-02-10 19:23       ` Paul Eggert
2021-02-10 19:45         ` Eli Zaretskii
2021-02-10 22:39           ` Matt Armstrong
2021-02-12  7:43             ` Eli Zaretskii
2021-02-12  9:36               ` Paul Eggert
2021-02-12 11:33                 ` Eli Zaretskii
2021-02-12 23:59                   ` Matt Armstrong
2021-02-13  8:07                     ` Eli Zaretskii
2021-02-11 22:14         ` Matt Armstrong
2021-02-12  2:20           ` Paul Eggert
2021-02-12  7:15             ` Eli Zaretskii
2021-02-13  1:15               ` Matt Armstrong
2021-02-13  1:26                 ` Paul Eggert
2021-02-13  8:21                   ` Eli Zaretskii
2021-02-13  8:28                 ` Eli Zaretskii
2021-02-14  0:49                   ` Matt Armstrong
2021-02-14 19:22                     ` Eli Zaretskii
2021-02-14 22:16                       ` Matt Armstrong
2021-02-15 15:09                         ` Eli Zaretskii
2021-02-16  0:49                           ` Matt Armstrong
2021-02-16  1:55                             ` Paul Eggert
2021-02-16 15:06                               ` Eli Zaretskii
2021-02-16 11:53                             ` Lars Ingebrigtsen
2021-02-22 19:24                             ` bug#46397: [PATCH] " Matt Armstrong
2021-02-19 19:10                           ` Matt Armstrong
2021-02-19 19:23                             ` Eli Zaretskii
2021-02-19 21:46                               ` Matt Armstrong
2021-02-20  9:09                                 ` Eli Zaretskii
2021-02-21  0:36                                   ` Matt Armstrong
2021-02-21 23:43                                     ` Mike Kupfer
2021-02-22  1:42                                       ` Matt Armstrong
2021-03-14 18:03                                         ` Bill Wohler
2021-03-17 23:36                                           ` Matt Armstrong
2021-02-24 17:37                                     ` Matt Armstrong
2021-02-24 18:50                                       ` Eli Zaretskii
2021-03-01 16:59                                       ` Eli Zaretskii
2021-03-05 22:19                                         ` Matt Armstrong
2021-03-06  9:36                                           ` Eli Zaretskii
2021-03-06 23:39                                             ` Matt Armstrong
2021-03-07  2:50                                             ` Paul Eggert
2021-03-07  5:57                                               ` Matt Armstrong
2021-02-19 19:45                             ` Paul Eggert
2021-02-19 21:52                               ` Matt Armstrong
2021-03-08  2:18                               ` Matt Armstrong
2021-03-11 14:34                                 ` Eli Zaretskii
2021-03-17 23:49                                   ` Matt Armstrong
2021-03-17 23:51                                   ` Matt Armstrong
2021-03-20 10:43                                     ` Eli Zaretskii
2021-03-22  1:43                                       ` Matt Armstrong
2021-03-27  9:20                                         ` Eli Zaretskii
2021-02-10  0:26   ` Matt Armstrong

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).