0

I am trying to append a line to my crontab file. I know there are other ways to work around this problem, but still want to know what caused it. The command is run on raspberry pi 3 B+, raspbian lite is installed, with GNU ed 1.15, cron 3.0pl1-134+deb10u1.

The command that I'm stuck on is this:

$ echo -e 'a\n#asdf\n.\nwQ' | EDITOR=ed crontab -e
902
909
No modification made

I'm expecting it to add line #asdf at the end of my crontab file, but it doesn't.

Setting EDITOR='tee -a' as suggested on https://stackoverflow.com/a/30123606/8842387 does not solve the problem. So I guess it is the problem with cron.

Strangely enough, when I give ed commands from the keyboard directly, rather than streaming it, it just works. Maybe subshell creation caused the problem?

Here I'm attaching a few of the last lines from strace result.

$ echo -e 'a\n#asdf\n.\nwQ' | EDITOR=ed strace crontab -e
execve("/usr/bin/crontab", ["crontab", "-e"], 0x7ee54c14 /* 29 vars */) = 0
access("/etc/suid-debug", F_OK)         = -1 ENOENT (No such file or directory)
...
read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\7\0\0\0\7\0\0\0\0"..., 4096) = 659
_llseek(3, -393, [266], SEEK_CUR)       = 0
read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\7\0\0\0\7\0\0\0\0"..., 4096) = 393
close(3)                                = 0
getpid()                                = 18579
socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 3
connect(3, {sa_family=AF_UNIX, sun_path="/dev/log"}, 110) = 0
send(3, "<78>Nov 20 15:31:25 crontab[1857"..., 56, MSG_NOSIGNAL) = 56
openat(AT_FDCWD, "crontabs/pi", O_RDONLY) = -1 EACCES (Permission denied)
openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 4
fstat64(4, {st_mode=S_IFREG|0644, st_size=2995, ...}) = 0
read(4, "# Locale name alias data base.\n#"..., 4096) = 2995
read(4, "", 4096)                       = 0
close(4)                                = 0
openat(AT_FDCWD, "/usr/share/locale/en_GB.UTF-8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/en_GB.utf8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/en_GB/LC_MESSAGES/libc.mo", O_RDONLY) = 4
fstat64(4, {st_mode=S_IFREG|0644, st_size=1433, ...}) = 0
mmap2(NULL, 1433, PROT_READ, MAP_PRIVATE, 4, 0) = 0x76f50000
close(4)                                = 0
openat(AT_FDCWD, "/usr/share/locale/en.UTF-8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/en.utf8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/en/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
write(2, "crontabs/pi/: fdopen: Permission"..., 39crontabs/pi/: fdopen: Permission denied) = 39
exit_group(1)                           = ?
+++ exited with 1 +++

openat(AT_FDCWD, "crontabs/pi", O_RDONLY) = -1 EACCES (Permission denied) looks a bit suspicious, but not sure why it opens the file read-only.

EDIT: As suggested by @tink, I ran EDITOR=ed strace crontab -e to see what strace gives on an interactive session. The result was almost same (only varying on pid and fd numbers).

I noticed that running echo "..." | EDITOR=ed crontab -e exited with message No modification made but with strace the process halts without any messages. (EDITOR=ed strace crontab -e 2>&1 | grep "No mod" prints nothing). Guess the strace triggers different errors.

bivoje
  • 23
  • 1
  • 7
  • Interesting ... could you please also paste what the "interactive" sessions' strace looks like? – tink Nov 20 '21 at 07:42
  • Not that I'm convinced that this is `programming` - personally I think this is a question better suited for [unix.se] or [su]. – tink Nov 20 '21 at 07:43
  • 2
    `crontab` is set-gid, but that's disabled when you run it from `strace`. That's why it gets "Permission denied" in the trace. It's not showing what's happening when you run it normally. – Barmar Nov 20 '21 at 08:09
  • 1
    I suggest you use `printf` instead of `echo -e`. It's more portable. – Barmar Nov 20 '21 at 08:10
  • I think `crontab` uses `$VISUAL` before `$EDITOR` -- do you have a VISUAL env var? – glenn jackman Nov 20 '21 at 15:26
  • @tink Thanks for your comment! I added more info on the question. By the way, is there a way to move the question to [Unix & Linux](https://unix.stackexchange.com/)? – bivoje Nov 21 '21 at 15:42
  • @Barmar I see, that's why it gave me the same error while running interactively. Any other way to inspect what is going on?` For other people reading this, I found this [stack overflow answer](https://stackoverflow.com/a/11530298) explaining why `printf` is more portable. ;) – bivoje Nov 21 '21 at 15:46
  • 1
    I know it's dangerous, but maybe if you did this as root it wouldn't mind that it can't change groups. – Barmar Nov 21 '21 at 16:26

2 Answers2

0

Following up on my VISUAL comment, these worked for me:

( unset VISUAL; printf '%s\n' a '#abcd' . wq | EDITOR=ed crontab -e )
printf '%s\n' a '#abcd' . wq | VISUAL=ed crontab -e

In my environment, both VISUAL and EDITOR are set to "vim"

Or, more roundabout, but don't need to monkey with env vars. This one also allows you to do it silently:

crontab <(printf '%s\n' a '#asdf' . '%p' | ed -s <(crontab -l))

I was doing the above on a Mac. On Linux, I can reproduce your observations, but can't explain them.

A small tweak to the last command works:

printf '%s\n' a '#asdf' . '%p' Q | ed -s <(crontab -l) | crontab -
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • Thanks for the answer! I'm afraid to say that neither of the above two lines worked for me and the last one gives `/dev/fd/63: Not a regular file.` Maybe some system configuration be different? – bivoje Nov 21 '21 at 15:55
0

TLDR; (sleep 1; echo -e 'a\n#asdf\n.\nwQ') | EDITOR=ed crontab -e works!

The problem was on crontab.

When I invoke crontab -e it creates a temporary copy of the user cron table in /tmp directory. Then opens the temporary file with an editor specified by $EDITOR. After the editing is done, crontab check if the file modification date have changed since its creation. This is implemented in the patch that enables editing cron table via temporary file.

In my case, ed getting its command from stdin finished the editing too fast so that even a single digit of the modification timestamp of the temporary file had not been changed. As crontab considered no human can make edition that fast, it assumes no modification made and discards it.

To bypass this behavior, I added sleep 1 before the release of the command. This will hold ed to wait for its command from stdin after crontab created tempfile, which effectively lets the modification timestamp different.

bivoje
  • 23
  • 1
  • 7
  • Why not simply `(crontab -l; echo new stuff) | crontab -` instead? – tripleee Dec 28 '21 at 11:23
  • @tripleee That's another way to go! Simple and nice solution. I had been using a similar - but dirtier - workaround till I locate the problem. Besides, my question was on curiosity, "why doesn't this work while seems obvious?" rather than "how to do this?". – bivoje Jan 03 '22 at 01:57