aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-25 13:32:22 -0400
committerCraig Jennings <c@cjennings.net>2026-06-25 13:32:22 -0400
commit11049db5700067652469e3408c5f4d8faf8bb332 (patch)
tree8ecb3692d01e3096da1802becd55acdffef8135b /modules
parent4e48432c5020d545c57e3ff24c43bc291acb7250 (diff)
downloaddotemacs-11049db5700067652469e3408c5f4d8faf8bb332.tar.gz
dotemacs-11049db5700067652469e3408c5f4d8faf8bb332.zip
fix(calendar-sync): atomic writes, curl --fail, and zero-event vs garbage
Three robustness fixes from the config audit. (1) calendar-sync--write-file and --save-state now write a temp file in the same directory and rename it into place, so org-agenda or chime reading mid-write never sees a half-written calendar. (2) The two curl fetches gain --fail, so an HTTP 404/500 error page exits non-zero instead of flowing its HTML into conversion. (3) calendar-sync--parse-ics distinguishes a healthy zero-event calendar (a real iCalendar with BEGIN:VCALENDAR and no in-window events returns the header) from garbage (no VCALENDAR returns nil), so a near-empty calendar no longer reports "parse failed". New robustness tests; the empty-calendar boundary test updated to the corrected behavior. Verified against the live feed: all three calendars fetch and write cleanly.
Diffstat (limited to 'modules')
-rw-r--r--modules/calendar-sync.el42
1 files changed, 28 insertions, 14 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el
index 7ed14921f..c0e0e935a 100644
--- a/modules/calendar-sync.el
+++ b/modules/calendar-sync.el
@@ -255,8 +255,10 @@ Example: -21600 → `UTC-6' or `UTC-6:00'."
(dir (file-name-directory calendar-sync--state-file)))
(unless (file-directory-p dir)
(make-directory dir t))
- (with-temp-file calendar-sync--state-file
- (prin1 state (current-buffer)))))
+ (let ((tmp (make-temp-file (expand-file-name ".calendar-sync-state-" dir))))
+ (with-temp-file tmp
+ (prin1 state (current-buffer)))
+ (rename-file tmp calendar-sync--state-file t))))
(defun calendar-sync--load-state ()
"Load sync state from disk."
@@ -1248,11 +1250,19 @@ RECURRENCE-ID exceptions are applied to override specific occurrences."
(time-less-p (calendar-sync--event-start-time a)
(calendar-sync--event-start-time b)))))
(org-entries (mapcar #'calendar-sync--event-to-org sorted-events)))
- (if org-entries
- (concat "# Calendar Events\n\n"
- (string-join org-entries "\n\n")
- "\n")
- nil)))
+ ;; Distinguish a healthy zero-event calendar from garbage: a real
+ ;; iCalendar (carries BEGIN:VCALENDAR) with no in-window events
+ ;; returns the header alone, so the caller writes an empty calendar
+ ;; and reports success. Non-iCalendar content (an HTML error page, a
+ ;; truncated download) has no VCALENDAR and returns nil -- a failure.
+ (cond
+ (org-entries
+ (concat "# Calendar Events\n\n"
+ (string-join org-entries "\n\n")
+ "\n"))
+ ((string-match-p "BEGIN:VCALENDAR" ics-content)
+ "# Calendar Events\n\n")
+ (t nil))))
(error
(calendar-sync--log-silently "calendar-sync: Parse error: %s" (error-message-string err))
nil)))
@@ -1271,7 +1281,7 @@ invoked when the fetch completes, either successfully or with an error."
(make-process
:name "calendar-sync-curl"
:buffer buffer
- :command (list "curl" "-s" "-L"
+ :command (list "curl" "-s" "-L" "--fail"
"--connect-timeout" "10"
"--max-time" (number-to-string calendar-sync-fetch-timeout)
url)
@@ -1303,7 +1313,7 @@ owns deleting the temp file after a successful callback."
(make-process
:name "calendar-sync-curl"
:buffer buffer
- :command (list "curl" "-s" "-L"
+ :command (list "curl" "-s" "-L" "--fail"
"--connect-timeout" "10"
"--max-time" (number-to-string calendar-sync-fetch-timeout)
"-o" temp-file
@@ -1329,13 +1339,17 @@ owns deleting the temp file after a successful callback."
(funcall callback nil))))
(defun calendar-sync--write-file (content file)
- "Write CONTENT to FILE.
-Creates parent directories if needed."
+ "Write CONTENT to FILE atomically.
+Creates parent directories if needed, then writes a temp file in the same
+directory and renames it into place, so org-agenda or chime reading mid-write
+never sees a half-written calendar."
(let ((dir (file-name-directory file)))
(unless (file-directory-p dir)
- (make-directory dir t)))
- (with-temp-file file
- (insert content)))
+ (make-directory dir t))
+ (let ((tmp (make-temp-file (expand-file-name ".calendar-sync-" dir))))
+ (with-temp-file tmp
+ (insert content))
+ (rename-file tmp file t))))
(defun calendar-sync--emacs-binary ()
"Return the Emacs executable to use for calendar conversion workers."