From e401663b91327b6ab44b785c776cf7bb9b227efb Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 28 Jun 2026 03:00:55 -0400 Subject: fix(transcription): write stderr to the error log instead of a phantom buffer make-process :stderr was a file path, but :stderr takes a buffer, so Emacs made a buffer named after the path instead of writing the file. The "Errored. Logs in " notification pointed at a log with no error text, and that hidden buffer leaked one per transcription. I now route stderr through an explicit, erased buffer: passed to :stderr, threaded to the sentinel, drained into the log, then killed. That keeps stderr off the stdout :buffer, so the transcript stays clean. --- modules/transcription-config.el | 27 ++++++++++--- tests/test-transcription-process-and-sentinel.el | 50 ++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/modules/transcription-config.el b/modules/transcription-config.el index e00306d1e..944063b88 100644 --- a/modules/transcription-config.el +++ b/modules/transcription-config.el @@ -195,6 +195,8 @@ transcript lands alongside the source, not next to the temp /tmp audio." (txt-file (car outputs)) (log-file (cdr outputs)) (buffer-name (format " *transcribe-%s*" (file-name-nondirectory audio-file))) + (stderr-buffer-name (format " *transcribe-stderr-%s*" + (file-name-nondirectory audio-file))) (process-name (format "transcribe-%s" (file-name-nondirectory audio-file)))) (unless (file-executable-p script) @@ -203,15 +205,25 @@ transcript lands alongside the source, not next to the temp /tmp audio." (cj/--init-log-file log-file audio-file script) (let* ((process-environment (cj/--build-process-environment cj/transcribe-backend)) + ;; A live, explicitly-managed buffer for stderr. Passing a file PATH + ;; to :stderr makes Emacs create a phantom buffer named after the + ;; path, so the error text never reaches the log file and that buffer + ;; leaks per run; the sentinel drains this buffer into the log and + ;; kills it. Keeping stderr off the stdout :buffer leaves the + ;; transcript (stdout) clean. + (stderr-buffer (with-current-buffer (get-buffer-create stderr-buffer-name) + (erase-buffer) + (current-buffer))) (process (make-process :name process-name :buffer (get-buffer-create buffer-name) :command (list script audio-file) :sentinel (lambda (proc event) - (cj/--transcription-sentinel proc event audio-file txt-file log-file) + (cj/--transcription-sentinel proc event audio-file + txt-file log-file stderr-buffer) (when cleanup-file (ignore-errors (delete-file cleanup-file)))) - :stderr log-file))) + :stderr stderr-buffer))) (cj/--track-transcription process audio-file) (cj/--notify "Transcription" (format "Started on %s" (file-name-nondirectory audio-file))) @@ -294,20 +306,25 @@ References TXT-FILE on success (normal urgency), LOG-FILE on failure (format "Errored. Logs in %s" (file-name-nondirectory log-file)) 'critical))) -(defun cj/--transcription-sentinel (process event _audio-file txt-file log-file) +(defun cj/--transcription-sentinel (process event _audio-file txt-file log-file stderr-buffer) "Sentinel for transcription PROCESS. EVENT is the process event string. TXT-FILE and LOG-FILE are the -associated output files." +associated output files. STDERR-BUFFER holds the process's stderr; its +contents are appended to LOG-FILE so the \"Logs in \" notification +points at real error text, and the buffer is then killed so it does not +leak per run." (let* ((success-p (and (string-match-p "finished" event) (= 0 (process-exit-status process)))) (process-buffer (process-buffer process))) (cj/--write-transcript-on-success process-buffer success-p txt-file) - (cj/--append-to-log process-buffer log-file event) + (cj/--append-to-log stderr-buffer log-file event) (cj/--update-transcription-status process success-p) (when (and success-p (not (cj/--should-keep-log success-p))) (delete-file log-file)) (when (buffer-live-p process-buffer) (kill-buffer process-buffer)) + (when (buffer-live-p stderr-buffer) + (kill-buffer stderr-buffer)) (cj/--notify-completion success-p txt-file log-file) (run-at-time 600 nil #'cj/--cleanup-completed-transcriptions) (force-mode-line-update t))) diff --git a/tests/test-transcription-process-and-sentinel.el b/tests/test-transcription-process-and-sentinel.el index 90b56f0a5..185412934 100644 --- a/tests/test-transcription-process-and-sentinel.el +++ b/tests/test-transcription-process-and-sentinel.el @@ -96,6 +96,35 @@ the script and the audio path." (should (equal (plist-get make-process-args :command) (list script audio))))) +(ert-deftest test-tx-start-process-stderr-is-a-buffer-not-a-path () + "Normal: :stderr is a live buffer, not a file path. +Passing a path string makes Emacs create a phantom buffer named after the +path, so stderr never reaches the log file and that buffer leaks per run." + (let* ((audio (make-temp-file "cj-tx-audio-" nil ".mp3")) + (script (make-temp-file "cj-tx-script-")) + (cj/transcriptions-list nil) + make-process-args) + (set-file-modes script #o755) + (unwind-protect + (cl-letf (((symbol-function 'cj/--transcription-script-path) + (lambda () script)) + ((symbol-function 'cj/--init-log-file) #'ignore) + ((symbol-function 'cj/--build-process-environment) + (lambda (_) '("FOO=bar"))) + ((symbol-function 'make-process) + (lambda (&rest kwargs) + (setq make-process-args kwargs) + 'fake-process)) + ((symbol-function 'cj/--notify) #'ignore) + ((symbol-function 'force-mode-line-update) #'ignore)) + (cj/--start-transcription-process audio)) + (delete-file audio) + (delete-file script)) + (let ((stderr (plist-get make-process-args :stderr))) + (should (bufferp stderr)) + (should (buffer-live-p stderr)) + (when (buffer-live-p stderr) (kill-buffer stderr))))) + ;;; cj/--transcription-sentinel (ert-deftest test-tx-sentinel-success-writes-transcript-and-updates-status () @@ -105,6 +134,7 @@ the entry status to `complete', and fires a normal-urgency notification." (log-file (make-temp-file "cj-tx-log-" nil ".log")) (process-buffer (generate-new-buffer " *cj-tx-test*")) (proc (list 'mock-process)) + (stderr-buffer (generate-new-buffer " *cj-tx-test-stderr*")) (cj/transcriptions-list (list (list proc "/tmp/audio.mp3" (current-time) 'running))) notify-urgency) @@ -121,12 +151,15 @@ the entry status to `complete', and fires a normal-urgency notification." (lambda (_t _m &optional u) (setq notify-urgency u)))) (cj/--transcription-sentinel proc "finished\n" "/tmp/audio.mp3" - txt-file log-file)) + txt-file log-file stderr-buffer)) (when (buffer-live-p process-buffer) (kill-buffer process-buffer)) + (when (buffer-live-p stderr-buffer) (kill-buffer stderr-buffer)) (delete-file txt-file) (delete-file log-file)) ;; success notification uses default (nil/normal) urgency. (should-not notify-urgency) + ;; the stderr buffer is drained and killed, never leaked. + (should-not (buffer-live-p stderr-buffer)) ;; entry status updated to complete. (let ((entry (car cj/transcriptions-list))) (should (eq (nth 3 entry) 'complete))))) @@ -138,10 +171,14 @@ marks the entry as `error'." (log-file (make-temp-file "cj-tx-log-" nil ".log")) (process-buffer (generate-new-buffer " *cj-tx-fail*")) (proc (list 'mock-fail)) + (stderr-buffer (generate-new-buffer " *cj-tx-fail-stderr*")) (cj/transcriptions-list (list (list proc "/tmp/audio.mp3" (current-time) 'running))) + log-contents notify-urgency) - (with-current-buffer process-buffer (insert "stderr blob")) + (with-current-buffer process-buffer (insert "partial transcript")) + (with-current-buffer stderr-buffer (insert "whisper: CUDA out of memory")) + (with-temp-file log-file (insert "HEADER\n")) (unwind-protect (cl-letf (((symbol-function 'process-buffer) (lambda (_) process-buffer)) @@ -153,11 +190,18 @@ marks the entry as `error'." (lambda (_t _m &optional u) (setq notify-urgency u)))) (cj/--transcription-sentinel proc "exited abnormally\n" "/tmp/audio.mp3" - txt-file log-file)) + txt-file log-file stderr-buffer) + (setq log-contents + (with-temp-buffer (insert-file-contents log-file) (buffer-string)))) (when (buffer-live-p process-buffer) (kill-buffer process-buffer)) + (when (buffer-live-p stderr-buffer) (kill-buffer stderr-buffer)) (delete-file txt-file) (delete-file log-file)) (should (eq notify-urgency 'critical)) + ;; the actual stderr error text reaches the log on failure. + (should (string-match-p "CUDA out of memory" log-contents)) + ;; the stderr buffer is killed, never leaked. + (should-not (buffer-live-p stderr-buffer)) (let ((entry (car cj/transcriptions-list))) (should (eq (nth 3 entry) 'error))))) -- cgit v1.2.3