Fix QTerminalWidget double-free crashes and complete Phase 5 behavioral tests
ober
a053941ff3e592f57ea49a1e476b6048ed3079b2
--- a/lib/jerboa-emacs/async.sls +++ b/lib/jerboa-emacs/async.sls @@ -552,8 +552,8 @@ (let ([v (hashtable-ref cache key - '#{miss j40h7yge6hu7zd2spbldcd8pr-1})]) - (if (eq? v '#{miss j40h7yge6hu7zd2spbldcd8pr-2}) + '#{miss bbhoe7u0vfcvf8wf5ezuvxl6i-1})]) + (if (eq? v '#{miss bbhoe7u0vfcvf8wf5ezuvxl6i-2}) (if (null? default) #f (car default)) v))) (def (weak-cache-set! cache key value) --- a/lib/jerboa-emacs/qt/app.sls +++ b/lib/jerboa-emacs/qt/app.sls @@ -1553,8 +1553,15 @@ [qt-term (and qt-buf (hash-get *terminal-widget-map* - qt-buf))]) + qt-buf))] + [key-src-widget (qt-last-key-widget)] + [key-from-terminal? (and qt-term + (equal? + key-src-widget + (qt-terminal-widget + qt-term)))]) (if (and qt-term + key-from-terminal? (null? (key-state-prefix-keys (app-state-key-state @@ -1814,7 +1821,25 @@ (hash-get *terminal-widget-map* buf))]) (if term (qt-terminal-widget term) - (qt-current-editor fr)))))) + (qt-current-editor fr))))) + (qt-window-set-pre-container-destroy-fn! + (lambda (container) + (let ([bufs-to-remove '()]) + (hash-for-each + (lambda (buf stored-container) + (when (equal? stored-container container) + (let ([term (hash-get *terminal-widget-map* buf)]) + (when term + (with-catch + (lambda (e) #f) + (lambda () (qt-terminal-destroy! term))))) + (set! bufs-to-remove (cons buf bufs-to-remove)))) + *terminal-container-map*) + (for-each + (lambda (buf) + (hash-remove! *terminal-widget-map* buf) + (hash-remove! *terminal-container-map* buf)) + bufs-to-remove))))) (schedule-periodic! 'repl-poll 50 @@ -2521,16 +2546,25 @@ 'test-terminal-running? (lambda () (let* ([fr (app-state-frame app)] - [buf (qt-current-buffer fr)]) - (and buf (hash-key? *terminal-widget-map* buf) #t)))) + [win (qt-current-window fr)] + [buf (qt-edit-window-buffer win)] + [term (and buf + (hash-get + *terminal-widget-map* + buf))]) + (and term + (let* ([container (qt-edit-window-container + win)] + [count (qt-stacked-widget-count + container)]) + (and (> count 1) + (> (qt-stacked-widget-current-index + container) + 0))))))) (cons 'test-reset! (lambda () (app-state-key-state-set! app (make-initial-key-state)) - (when (> (length - (qt-frame-windows (app-state-frame app))) - 1) - (execute-command! app 'delete-other-windows)) (let ([term-bufs (hash-keys *terminal-widget-map*)]) (for-each (lambda (buf) @@ -2541,8 +2575,15 @@ (with-catch (lambda (e) #f) (lambda () (qt-terminal-destroy! term))) - (hash-remove! *terminal-widget-map* buf)))) + (hash-remove! *terminal-widget-map* buf) + (hash-remove! + *terminal-container-map* + buf)))) term-bufs)) + (when (> (length + (qt-frame-windows (app-state-frame app))) + 1) + (execute-command! app 'delete-other-windows)) 'ok)) (cons 'test-clear-buffer! --- a/lib/jerboa-emacs/qt/commands-config.sls +++ b/lib/jerboa-emacs/qt/commands-config.sls @@ -938,6 +938,7 @@ (qt-terminal-widget term))]) (qt-stacked-widget-set-current-index! container idx)) (hash-put! *terminal-widget-map* buf term) + (hash-put! *terminal-container-map* buf container) ((app-state-key-handler app) (qt-terminal-widget term)) (qt-terminal-spawn! term "") (qt-terminal-focus! term) --- a/lib/jerboa-emacs/qt/commands-shell.sls +++ b/lib/jerboa-emacs/qt/commands-shell.sls @@ -4,30 +4,30 @@ (library (jerboa-emacs qt commands-shell) (export directory-exists? *terminal-widget-map* - apply-font-size-to-all-editors! cmd-increase-font-size - cmd-decrease-font-size cmd-reset-font-size - cmd-goto-first-non-blank cmd-goto-last-non-blank - cmd-move-to-window-top cmd-move-to-window-middle - cmd-move-to-window-bottom cmd-scroll-left cmd-scroll-right - cmd-insert-let cmd-insert-lambda cmd-insert-defun - cmd-insert-cond cmd-insert-when cmd-insert-unless - cmd-insert-match cmd-insert-import cmd-insert-export - cmd-insert-include cmd-insert-file-header - cmd-insert-header-guard cmd-insert-box-comment - cmd-insert-file-contents cmd-insert-register-string - *auto-indent* *backup-files* *version-control* *debug-mode* - *debug-on-quit* *visible-bell* *transient-mark* - *electric-indent* cmd-toggle-auto-indent - cmd-toggle-backup-files cmd-toggle-version-control - cmd-toggle-debug-mode cmd-toggle-debug-on-quit - cmd-toggle-visible-bell cmd-toggle-transient-mark - cmd-toggle-electric-indent cmd-toggle-auto-revert - cmd-toggle-auto-revert-global cmd-auto-revert-tail-mode - *view-mode-buffers* cmd-view-mode *so-long-threshold* - *so-long-buffers* check-so-long! cmd-so-long-mode - *follow-mode* cmd-follow-mode *command-history-file* - savehist-save! savehist-load! auto-fill-check! - qt-aggressive-indent-line! + *terminal-container-map* apply-font-size-to-all-editors! + cmd-increase-font-size cmd-decrease-font-size + cmd-reset-font-size cmd-goto-first-non-blank + cmd-goto-last-non-blank cmd-move-to-window-top + cmd-move-to-window-middle cmd-move-to-window-bottom + cmd-scroll-left cmd-scroll-right cmd-insert-let + cmd-insert-lambda cmd-insert-defun cmd-insert-cond + cmd-insert-when cmd-insert-unless cmd-insert-match + cmd-insert-import cmd-insert-export cmd-insert-include + cmd-insert-file-header cmd-insert-header-guard + cmd-insert-box-comment cmd-insert-file-contents + cmd-insert-register-string *auto-indent* *backup-files* + *version-control* *debug-mode* *debug-on-quit* + *visible-bell* *transient-mark* *electric-indent* + cmd-toggle-auto-indent cmd-toggle-backup-files + cmd-toggle-version-control cmd-toggle-debug-mode + cmd-toggle-debug-on-quit cmd-toggle-visible-bell + cmd-toggle-transient-mark cmd-toggle-electric-indent + cmd-toggle-auto-revert cmd-toggle-auto-revert-global + cmd-auto-revert-tail-mode *view-mode-buffers* cmd-view-mode + *so-long-threshold* *so-long-buffers* check-so-long! + cmd-so-long-mode *follow-mode* cmd-follow-mode + *command-history-file* savehist-save! savehist-load! + auto-fill-check! qt-aggressive-indent-line! cmd-toggle-delete-trailing-whitespace-on-save uniquify-parent-suffix uniquify-buffer-name! cmd-recentf-open-files cmd-toggle-frame-fullscreen @@ -118,6 +118,8 @@ (and (file-exists? path) (file-directory? path))) (define *terminal-widget-map*--cell (vector (make-hash-table-eq))) + (define *terminal-container-map*--cell + (vector (make-hash-table-eq))) (def (apply-font-size-to-all-editors! app) "Apply the current global font size to all open editors." (let ([fr (app-state-frame app)] @@ -2280,6 +2282,13 @@ *terminal-widget-map*--cell 0 val)])) + (define-syntax *terminal-container-map* + (identifier-syntax + [id (vector-ref *terminal-container-map*--cell 0)] + [(set! id val) (vector-set! + *terminal-container-map*--cell + 0 + val)])) (define-syntax *auto-indent* (identifier-syntax [id (vector-ref *auto-indent*--cell 0)] --- a/lib/jerboa-emacs/qt/commands.sls +++ b/lib/jerboa-emacs/qt/commands.sls @@ -5,10 +5,10 @@ (library (jerboa-emacs qt commands) (export qt-register-all-commands! dired-open-directory! qt-open-image-inline! *qt-app-ptr* *terminal-widget-map* - qt-kill-ring-push! *isearch-active* isearch-handle-key! - *qreplace-active* qreplace-handle-key! recent-files-add! - recent-files-load! bookmarks-load! session-save! - session-restore-files *tab-bar-visible* + *terminal-container-map* qt-kill-ring-push! *isearch-active* + isearch-handle-key! *qreplace-active* qreplace-handle-key! + recent-files-add! recent-files-load! bookmarks-load! + session-save! session-restore-files *tab-bar-visible* *auto-revert-tail-buffers* *file-mtimes* file-mtime-record! file-mtime-changed? *eldoc-mode* eldoc-display! *current-theme* *themes* theme-stylesheet load-theme! --- a/lib/jerboa-emacs/qt/sci-shim.sls +++ b/lib/jerboa-emacs/qt/sci-shim.sls @@ -77,13 +77,14 @@ qt-label-set-alignment! qt-label-set-pixmap! qt-label-set-text! qt-label-text qt-last-key-code qt-last-key-modifiers qt-last-key-text - qt-last-key-autorepeat? qt-line-edit-create - qt-line-edit-set-completer! qt-line-edit-set-text! - qt-line-edit-text qt-list-widget-add-item! - qt-list-widget-clear! qt-list-widget-create - qt-list-widget-current-row qt-list-widget-set-current-row! - qt-main-window-add-toolbar! qt-main-window-create - qt-main-window-menu-bar qt-main-window-set-central-widget! + qt-last-key-autorepeat? qt-last-key-widget + qt-line-edit-create qt-line-edit-set-completer! + qt-line-edit-set-text! qt-line-edit-text + qt-list-widget-add-item! qt-list-widget-clear! + qt-list-widget-create qt-list-widget-current-row + qt-list-widget-set-current-row! qt-main-window-add-toolbar! + qt-main-window-create qt-main-window-menu-bar + qt-main-window-set-central-widget! qt-main-window-set-status-bar-text! qt-main-window-set-title! qt-menu-add-action! qt-menu-add-separator! qt-menu-bar-add-menu qt-on-clicked! --- a/lib/jerboa-emacs/qt/window.sls +++ b/lib/jerboa-emacs/qt/window.sls @@ -28,7 +28,8 @@ qt-frame-other-window! qt-apply-editor-theme! split-tree-flatten split-tree-find-parent split-tree-find-leaf split-tree-collect-sub-splitters - qt-window-set-app-ptr!) + qt-window-set-app-ptr! + qt-window-set-pre-container-destroy-fn!) (import (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex @@ -40,6 +41,12 @@ (def *qt-app-for-events* #f) (def (qt-window-set-app-ptr! app) (set! *qt-app-for-events* app)) + (def *pre-container-destroy-fn* #f) + (def (qt-window-set-pre-container-destroy-fn! fn) + (set! *pre-container-destroy-fn* fn)) + (def (qt-window-pre-container-destroy! container) + (when *pre-container-destroy-fn* + (*pre-container-destroy-fn* container))) (def (qt-window-process-events!) "Process pending Qt events if app pointer is available." (let ([app *qt-app-for-events*]) @@ -546,6 +553,7 @@ (set! container #f) (qt-widget-destroy! parent-spl)))))) (when container + (qt-window-pre-container-destroy! container) (qt-widget-hide! container) (qt-widget-destroy! container)) (qt-frame-windows-set! @@ -578,6 +586,7 @@ (let ([ed (qt-edit-window-editor win)] [container (qt-edit-window-container win)]) (hash-remove! *editor-window-map* ed) + (qt-window-pre-container-destroy! container) (qt-widget-hide! container) (qt-widget-destroy! container)))) all-wins) --- a/src/jerboa-emacs/qt/app.ss +++ b/src/jerboa-emacs/qt/app.ss @@ -1255,9 +1255,20 @@ ;; QTerminalWidget key forwarding: send non-command keys directly ;; to the terminal widget, bypassing chord detection and self-insert. ;; C-x prefix and M-x pass through to jemacs keymap. + ;; + ;; FOCUS GUARD: only forward to the terminal if the key event came + ;; FROM the QTerminalWidget itself. If the user clicked a different + ;; window (so the key came from that window's QScintilla), we must + ;; NOT forward to the terminal even if qt-current-buffer is still + ;; a terminal buffer (Chez state may lag the Qt focus change). (let* ((qt-buf (qt-current-buffer (app-state-frame app))) - (qt-term (and qt-buf (hash-get *terminal-widget-map* qt-buf)))) + (qt-term (and qt-buf (hash-get *terminal-widget-map* qt-buf))) + (key-src-widget (qt-last-key-widget)) + (key-from-terminal? (and qt-term + (equal? key-src-widget + (qt-terminal-widget qt-term))))) (if (and qt-term + key-from-terminal? ;; key must come FROM this terminal widget ;; Not in a prefix key state (e.g. after C-x) (null? (key-state-prefix-keys (app-state-key-state app))) ;; Allow C-x to pass through to jemacs @@ -1391,7 +1402,30 @@ (term (and buf (hash-get *terminal-widget-map* buf)))) (if term (qt-terminal-widget term) - (qt-current-editor fr)))))) + (qt-current-editor fr))))) + + ;; Install pre-container-destroy hook so that when any window container + ;; (QStackedWidget) is about to be destroyed (by delete-other-windows, + ;; C-x 0, kill-terminal-buffer, etc.), any terminal living inside it + ;; is detached and destroyed first — preventing the double-free crash + ;; that occurs when Qt auto-deletes the terminal as a child. + (qt-window-set-pre-container-destroy-fn! + (lambda (container) + (let ((bufs-to-remove '())) + (hash-for-each + (lambda (buf stored-container) + (when (equal? stored-container container) + (let ((term (hash-get *terminal-widget-map* buf))) + (when term + (with-catch (lambda (e) #f) + (lambda () (qt-terminal-destroy! term))))) + (set! bufs-to-remove (cons buf bufs-to-remove)))) + *terminal-container-map*) + (for-each + (lambda (buf) + (hash-remove! *terminal-widget-map* buf) + (hash-remove! *terminal-container-map* buf)) + bufs-to-remove))))) ;; ================================================================ ;; Periodic tasks — registered with schedule-periodic!, driven @@ -1925,20 +1959,31 @@ (not (null? (key-state-prefix-keys (app-state-key-state app)))))) ;; Is the current buffer a QTerminalWidget terminal? (cons 'test-terminal-running? + ;; True only if the current window's QStackedWidget is actually + ;; showing the QTerminalWidget page (not just the buffer being a + ;; terminal buffer — after C-x 2, new window shows editor page). (lambda () - (let* ((fr (app-state-frame app)) - (buf (qt-current-buffer fr))) - (and buf (hash-key? *terminal-widget-map* buf) #t)))) + (let* ((fr (app-state-frame app)) + (win (qt-current-window fr)) + (buf (qt-edit-window-buffer win)) + (term (and buf (hash-get *terminal-widget-map* buf)))) + (and term + (let* ((container (qt-edit-window-container win)) + (count (qt-stacked-widget-count container))) + ;; Terminal widget was added as the last page (index count-1). + ;; If count > 1 and current index > 0, terminal is visible. + (and (> count 1) + (> (qt-stacked-widget-current-index container) 0))))))) ;; Reset editor to a clean single-window state between tests. ;; Clears key prefix state, collapses to one window, destroys terminals. (cons 'test-reset! (lambda () ;; Clear any pending prefix key (e.g. C-x) (set! (app-state-key-state app) (make-initial-key-state)) - ;; Collapse to single window - (when (> (length (qt-frame-windows (app-state-frame app))) 1) - (execute-command! app 'delete-other-windows)) - ;; Destroy all QTerminalWidget instances + ;; Destroy terminals BEFORE delete-other-windows: qt-terminal-destroy! + ;; detaches the widget from its parent QStackedWidget, preventing the + ;; double-free that occurs when delete-other-windows destroys the + ;; container and Qt auto-deletes its children. (let ((term-bufs (hash-keys *terminal-widget-map*))) (for-each (lambda (buf) @@ -1946,8 +1991,12 @@ (when term (with-catch (lambda (e) #f) (lambda () (qt-terminal-destroy! term))) - (hash-remove! *terminal-widget-map* buf)))) + (hash-remove! *terminal-widget-map* buf) + (hash-remove! *terminal-container-map* buf)))) term-bufs)) + ;; Collapse to single window (safe now: terminals detached from containers) + (when (> (length (qt-frame-windows (app-state-frame app))) 1) + (execute-command! app 'delete-other-windows)) 'ok)) ;; Set current editor text directly (bypasses undo — for test isolation) (cons 'test-clear-buffer! --- a/src/jerboa-emacs/qt/commands-config.ss +++ b/src/jerboa-emacs/qt/commands-config.ss @@ -795,6 +795,9 @@ modified so the next save uses the new encoding." (qt-stacked-widget-set-current-index! container idx)) ;; Store widget mapping for key forwarding and buffer switching (hash-put! *terminal-widget-map* buf term) + ;; Track which container this terminal lives in so the pre-destroy + ;; hook can detach it before the container is freed + (hash-put! *terminal-container-map* buf container) ;; Install consuming key filter so all keys go through Scheme first ;; (same pattern as image scroll widgets — without this, Qt sends keys ;; directly to QTerminalWidget::keyPressEvent, bypassing jemacs entirely) --- a/src/jerboa-emacs/qt/commands-shell.ss +++ b/src/jerboa-emacs/qt/commands-shell.ss @@ -65,6 +65,11 @@ ;; so moving it here makes it visible to both without circular dependency. (def *terminal-widget-map* (make-hash-table-eq)) +;; Map buffer → QStackedWidget container for the terminal's host window. +;; Lets the pre-container-destroy hook identify which terminal is in a container +;; that is about to be destroyed, so it can detach/destroy the terminal first. +(def *terminal-container-map* (make-hash-table-eq)) + ;; --- Font size --- ;; Note: Font size state is now in face.ss (*default-font-size*) --- a/src/jerboa-emacs/qt/commands.ss +++ b/src/jerboa-emacs/qt/commands.ss @@ -7,6 +7,7 @@ qt-open-image-inline! *qt-app-ptr* *terminal-widget-map* + *terminal-container-map* qt-kill-ring-push! *isearch-active* isearch-handle-key! --- a/src/jerboa-emacs/qt/sci-shim.ss +++ b/src/jerboa-emacs/qt/sci-shim.ss @@ -74,7 +74,7 @@ ;; Label qt-label-create qt-label-set-alignment! qt-label-set-pixmap! qt-label-set-text! qt-label-text ;; Key events - qt-last-key-code qt-last-key-modifiers qt-last-key-text qt-last-key-autorepeat? + qt-last-key-code qt-last-key-modifiers qt-last-key-text qt-last-key-autorepeat? qt-last-key-widget ;; Line edit qt-line-edit-create qt-line-edit-set-completer! qt-line-edit-set-text! qt-line-edit-text ;; List widget --- a/src/jerboa-emacs/qt/window.ss +++ b/src/jerboa-emacs/qt/window.ss @@ -39,7 +39,9 @@ split-tree-find-leaf split-tree-collect-sub-splitters ;; App pointer for process-events during splits - qt-window-set-app-ptr!) + qt-window-set-app-ptr! + ;; Hook called before any window container is destroyed + qt-window-set-pre-container-destroy-fn!) (import :std/sugar :chez-scintilla/constants @@ -58,6 +60,17 @@ (def (qt-window-set-app-ptr! app) (set! *qt-app-for-events* app)) +;; Called with (container) before qt-widget-destroy! on any window container. +;; Registered by terminal code to detach QTerminalWidgets before parent is freed. +(def *pre-container-destroy-fn* #f) + +(def (qt-window-set-pre-container-destroy-fn! fn) + (set! *pre-container-destroy-fn* fn)) + +(def (qt-window-pre-container-destroy! container) + (when *pre-container-destroy-fn* + (*pre-container-destroy-fn* container))) + (def (qt-window-process-events!) "Process pending Qt events if app pointer is available." (let ((app *qt-app-for-events*)) @@ -560,6 +573,7 @@ ;; Destroy the deleted window's Qt container. ;; Skipped when container was set to #f above (already destroyed via parent-spl). (when container + (qt-window-pre-container-destroy! container) (qt-widget-hide! container) (qt-widget-destroy! container)) @@ -587,6 +601,7 @@ (let ((ed (qt-edit-window-editor win)) (container (qt-edit-window-container win))) (hash-remove! *editor-window-map* ed) + (qt-window-pre-container-destroy! container) (qt-widget-hide! container) (qt-widget-destroy! container)))) all-wins) --- a/vendor/chez-qt/chez-qt/ffi.ss +++ b/vendor/chez-qt/chez-qt/ffi.ss @@ -212,7 +212,7 @@ ;; Keyboard Events ffi-qt-install-key-handler ffi-qt-install-key-handler-consuming ffi-qt-last-key-code ffi-qt-last-key-modifiers ffi-qt-last-key-text - ffi-qt-last-key-autorepeat + ffi-qt-last-key-autorepeat ffi-qt-last-key-widget ffi-qt-send-key-event ;; Pixmap @@ -1656,6 +1656,8 @@ (foreign-procedure "qt_last_key_text" () string)) (define ffi-qt-last-key-autorepeat (foreign-procedure "qt_last_key_autorepeat" () int)) + (define ffi-qt-last-key-widget + (foreign-procedure "qt_last_key_widget" () void*)) (define ffi-qt-send-key-event (foreign-procedure "qt_send_key_event" (void* int int int string) void)) --- a/vendor/chez-qt/chez-qt/qt.ss +++ b/vendor/chez-qt/chez-qt/qt.ss @@ -212,7 +212,7 @@ ;; Keyboard Events qt-on-key-press! qt-on-key-press-consuming! qt-last-key-code qt-last-key-modifiers qt-last-key-text - qt-last-key-autorepeat? + qt-last-key-autorepeat? qt-last-key-widget qt-send-key-press! qt-send-key-release! ;; Pixmap @@ -1565,6 +1565,7 @@ (define (qt-last-key-modifiers) (ffi-qt-last-key-modifiers)) (define (qt-last-key-text) (ffi-qt-last-key-text)) (define (qt-last-key-autorepeat?) (not (zero? (ffi-qt-last-key-autorepeat)))) + (define (qt-last-key-widget) (ffi-qt-last-key-widget)) (define (qt-send-key-press! w key mods text) (ffi-qt-send-key-event w 0 key mods text)) --- a/vendor/qt_shim.cpp +++ b/vendor/qt_shim.cpp @@ -601,6 +601,7 @@ static int s_last_key_code = 0; static int s_last_key_modifiers = 0; static std::string s_last_key_text; static int s_last_key_autorepeat = 0; +static QObject* s_last_key_widget = nullptr; // which widget fired the last key event // Storage for QInputDialog ok/cancel flag static bool s_last_input_ok = false; @@ -624,6 +625,7 @@ public: s_last_key_modifiers = static_cast<int>(ke->modifiers()); s_last_key_text = ke->text().toUtf8().toStdString(); s_last_key_autorepeat = ke->isAutoRepeat() ? 1 : 0; + s_last_key_widget = obj; m_callback(m_callback_id); } return QObject::eventFilter(obj, event); @@ -649,6 +651,7 @@ public: s_last_key_modifiers = static_cast<int>(ke->modifiers()); s_last_key_text = ke->text().toUtf8().toStdString(); s_last_key_autorepeat = ke->isAutoRepeat() ? 1 : 0; + s_last_key_widget = obj; m_callback(m_callback_id); return true; // consume the event — widget does NOT see it } @@ -2470,6 +2473,12 @@ extern "C" int qt_last_key_autorepeat(void) { QT_RETURN(int, s_last_key_autorepeat); } +// Returns the QObject* (widget pointer) that fired the last key event. +// Lets Chez determine whether a key came from a terminal widget or an editor. +extern "C" void* qt_last_key_widget(void) { + return s_last_key_widget; +} + extern "C" void qt_send_key_event(qt_widget_t w, int type, int key, int modifiers, const char* text) { QT_NULL_CHECK_VOID(w); // Capture primitives by value; construct QKeyEvent inside the lambda @@ -7510,6 +7519,10 @@ public: if (m_vt) { vterm_free(m_vt); m_vt = nullptr; } } + // Public alias so qt_terminal_destroy can call PTY cleanup before + // the widget is actually deleted (via deleteLater). + void cleanupPtyPublic() { cleanupPty(); } + // ── libvterm initialization ──────────────────────────────────────────── void initVterm() { @@ -7602,9 +7615,30 @@ public: void cleanupPty() { if (m_timer) m_timer->stop(); if (m_child_pid > 0) { - kill(m_child_pid, SIGTERM); + // Close master fd first — this sends SIGHUP to the child, which + // is gentler than SIGTERM and causes most shells to exit. + if (m_master_fd >= 0) { + ::close(m_master_fd); + m_master_fd = -1; + } + // Non-blocking check — child may have already exited on SIGHUP. int status; - waitpid(m_child_pid, &status, 0); + pid_t result = waitpid(m_child_pid, &status, WNOHANG); + if (result != m_child_pid) { + // Child still alive — escalate to SIGTERM then SIGKILL. + kill(m_child_pid, SIGTERM); + // Poll for up to 200ms with WNOHANG before giving up. + for (int i = 0; i < 20 && result != m_child_pid; ++i) { + struct timespec ts = {0, 10000000}; // 10ms + nanosleep(&ts, nullptr); + result = waitpid(m_child_pid, &status, WNOHANG); + } + if (result != m_child_pid) { + kill(m_child_pid, SIGKILL); + // One final non-blocking reap; orphan if still alive. + waitpid(m_child_pid, &status, WNOHANG); + } + } m_child_pid = -1; } if (m_master_fd >= 0) { @@ -7877,11 +7911,17 @@ private: if (m_master_fd < 0) return; char buf[8192]; - ssize_t n; + ssize_t n = 0; bool got_data = false; + // Cap reads per poll to ~64KB so high-output commands (find / -ls, etc.) + // don't monopolise the Qt event loop and block M-x / other key events. + static const size_t MAX_BYTES_PER_POLL = 65536; + size_t total = 0; - while ((n = ::read(m_master_fd, buf, sizeof(buf))) > 0) { + while (total < MAX_BYTES_PER_POLL && + (n = ::read(m_master_fd, buf, sizeof(buf))) > 0) { vterm_input_write(m_vt, buf, (size_t)n); + total += (size_t)n; got_data = true; } @@ -7996,7 +8036,18 @@ extern "C" void qt_terminal_focus(qt_terminal_t term) { extern "C" void qt_terminal_destroy(qt_terminal_t term) { QT_NULL_CHECK_VOID(term); - QT_VOID( - delete static_cast<QTerminalWidget*>(term) + // 1. Detach from parent (QStackedWidget) so delete-other-windows cannot + // double-free the widget when it later destroys the container. + // 2. Schedule deletion via the event loop (deleteLater) to avoid + // "shared QObject deleted directly" crash from pending Qt events. + // The destructor calls cleanupPty() which uses WNOHANG — no blocking. + QT_VOID( + auto* tw = static_cast<QTerminalWidget*>(term); + if (QWidget* p = tw->parentWidget()) { + if (auto* stacked = qobject_cast<QStackedWidget*>(p)) + stacked->removeWidget(tw); + tw->setParent(nullptr); + } + tw->deleteLater() ); }