(define-module (ice-9 gds-client) #:use-module (oop goops) #:use-module (oop goops describe) #:use-module (ice-9 debugging breakpoints) #:use-module (ice-9 debugging trace) #:use-module (ice-9 debugging traps) #:use-module (ice-9 debugging trc) #:use-module (ice-9 debugging steps) #:use-module (ice-9 pretty-print) #:use-module (ice-9 regex) #:use-module (ice-9 session) #:use-module (ice-9 string-fun) #:export (gds-debug-trap run-utility set-gds-breakpoints gds-accept-input)) (cond ((string>=? (version) "1.7") (use-modules (ice-9 debugger utils))) (else (define the-ice-9-debugger-module (resolve-module '(ice-9 debugger))) (module-export! the-ice-9-debugger-module '(source-position write-frame-short/application write-frame-short/expression write-frame-args-long write-frame-long)))) (use-modules (ice-9 debugger)) (define gds-port #f) ;; Return an integer that somehow identifies the current thread. (define (get-thread-id) (let ((root (dynamic-root))) (cond ((integer? root) root) ((pair? root) (object-address root)) (else (error "Unexpected dynamic root:" root))))) ;; gds-debug-read is a high-priority read. The (debug-thread-id ID) ;; form causes the frontend to dismiss any reads from threads whose id ;; is not ID, until it receives the (thread-id ...) form with the same ;; id as ID. Dismissing the reads of any other threads (by sending a ;; form that is otherwise ignored) causes those threads to release the ;; read mutex, which allows the (gds-read) here to proceed. (define (gds-debug-read) (write-form `(debug-thread-id ,(get-thread-id))) (gds-read)) (define (gds-debug-trap trap-context) "Invoke the GDS debugger to explore the stack at the specified trap." (connect-to-gds) (start-stack 'debugger (let* ((stack (tc:stack trap-context)) (flags1 (let ((trap-type (tc:type trap-context))) (case trap-type ((#:return #:error) (list trap-type (tc:return-value trap-context))) (else (list trap-type))))) (flags (if (tc:continuation trap-context) (cons #:continuable flags1) flags1)) (fired-traps (tc:fired-traps trap-context)) (special-index (and (= (length fired-traps) 1) (is-a? (car fired-traps) ) (eq? (tc:type trap-context) #:return) (- (tc:depth trap-context) (slot-ref (car fired-traps) 'depth))))) ;; Write current stack to the frontend. (write-form (list 'stack (or special-index 0) (stack->emacs-readable stack) (append (flags->emacs-readable flags) (slot-ref trap-context 'handler-return-syms)))) ;; Now wait for instruction. (let loop ((protocol (gds-debug-read))) ;; Act on it. (case (car protocol) ((tweak) ;; Request to tweak the handler return value. (let ((tweaking (catch #t (lambda () (list (with-input-from-string (cadr protocol) read))) (lambda ignored #f)))) (if tweaking (slot-set! trap-context 'handler-return-value (cons 'instead (car tweaking))))) (loop (gds-debug-read))) ((continue) ;; Continue (by exiting the debugger). *unspecified*) ((evaluate) ;; Evaluate expression in specified frame. (eval-in-frame stack (cadr protocol) (caddr protocol)) (loop (gds-debug-read))) ((info-frame) ;; Return frame info. (let ((frame (stack-ref stack (cadr protocol)))) (write-form (list 'info-result (with-output-to-string (lambda () (write-frame-long frame)))))) (loop (gds-debug-read))) ((info-args) ;; Return frame args. (let ((frame (stack-ref stack (cadr protocol)))) (write-form (list 'info-result (with-output-to-string (lambda () (write-frame-args-long frame)))))) (loop (gds-debug-read))) ((proc-source) ;; Show source of application procedure. (let* ((frame (stack-ref stack (cadr protocol))) (proc (frame-procedure frame)) (source (and proc (procedure-source proc)))) (write-form (list 'info-result (if source (sans-surrounding-whitespace (with-output-to-string (lambda () (pretty-print source)))) (if proc "This procedure is coded in C" "This frame has no procedure"))))) (loop (gds-debug-read))) ((traps-here) ;; Show the traps that fired here. (write-form (list 'info-result (with-output-to-string (lambda () (for-each describe (tc:fired-traps trap-context)))))) (loop (gds-debug-read))) ((step-into) ;; Set temporary breakpoint on next trap. (at-step gds-debug-trap 1 #f (if (memq #:return flags) #f (- (stack-length stack) (cadr protocol))))) ((step-over) ;; Set temporary breakpoint on exit from ;; specified frame. (at-exit (- (stack-length stack) (cadr protocol)) gds-debug-trap)) ((step-file) ;; Set temporary breakpoint on next trap in same ;; source file. (at-step gds-debug-trap 1 (frame-file-name (stack-ref stack (cadr protocol))) (if (memq #:return flags) #f (- (stack-length stack) (cadr protocol))))) (else (safely-handle-nondebug-protocol protocol) (loop (gds-debug-read)))))))) (define (connect-to-gds) (or gds-port (begin (set! gds-port (or (let ((s (socket PF_INET SOCK_STREAM 0)) (SOL_TCP 6) (TCP_NODELAY 1)) (setsockopt s SOL_TCP TCP_NODELAY 1) (catch #t (lambda () (connect s AF_INET (inet-aton "127.0.0.1") 8333) s) (lambda _ #f))) (let ((s (socket PF_UNIX SOCK_STREAM 0))) (catch #t (lambda () (connect s AF_UNIX "/tmp/.gds_socket") s) (lambda _ #f))) (error "Couldn't connect to GDS by TCP or Unix domain socket"))) (write-form (list 'name (getpid) (format #f "PID ~A" (getpid))))))) (if (not (defined? 'make-mutex)) (begin (define (make-mutex) #f) (define lock-mutex noop) (define unlock-mutex noop))) (define write-mutex (make-mutex)) (define (write-form form) ;; Write any form FORM to GDS. (lock-mutex write-mutex) (write form gds-port) (newline gds-port) (force-output gds-port) (unlock-mutex write-mutex)) (define (stack->emacs-readable stack) ;; Return Emacs-readable representation of STACK. (map (lambda (index) (frame->emacs-readable (stack-ref stack index))) (iota (min (stack-length stack) (cadr (memq 'depth (debug-options))))))) (define (frame->emacs-readable frame) ;; Return Emacs-readable representation of FRAME. (if (frame-procedure? frame) (list 'application (with-output-to-string (lambda () (display (if (frame-real? frame) " " "t ")) (write-frame-short/application frame))) (source->emacs-readable frame)) (list 'evaluation (with-output-to-string (lambda () (display (if (frame-real? frame) " " "t ")) (write-frame-short/expression frame))) (source->emacs-readable frame)))) (define (source->emacs-readable frame) ;; Return Emacs-readable representation of the filename, line and ;; column source properties of SOURCE. (or (frame->source-position frame) 'nil)) (define (flags->emacs-readable flags) ;; Return Emacs-readable representation of trap FLAGS. (let ((prev #f)) (map (lambda (flag) (let ((erf (if (and (keyword? flag) (not (eq? prev #:return))) (keyword->symbol flag) (format #f "~S" flag)))) (set! prev flag) erf)) flags))) (define (eval-in-frame stack index expr) (write-form (list 'eval-result (format #f "~S" (catch #t (lambda () (local-eval (with-input-from-string expr read) (memoized-environment (frame-source (stack-ref stack index))))) (lambda args (cons 'ERROR args))))))) (set! (behaviour-ordering gds-debug-trap) 100) ;;; Code below here adds support for interaction between the GDS ;;; client program and the Emacs frontend even when not stopped in the ;;; debugger. ;; A mutex to control attempts by multiple threads to read protocol ;; back from the frontend. (define gds-read-mutex (make-mutex)) ;; Read a protocol instruction from the frontend. (define (gds-read) ;; Acquire the read mutex. (lock-mutex gds-read-mutex) ;; Tell the front end something that identifies us as a thread. (write-form `(thread-id ,(get-thread-id))) ;; Now read, then release the mutex and return what was read. (let ((x (catch #t (lambda () (read gds-port)) (lambda ignored the-eof-object)))) (unlock-mutex gds-read-mutex) x)) (define (gds-accept-input exit-on-continue) ;; If reading from the GDS connection returns EOF, we will throw to ;; this catch. (catch 'server-eof (lambda () (let loop ((protocol (gds-read))) (if (or (eof-object? protocol) (and exit-on-continue (eq? (car protocol) 'continue))) (throw 'server-eof)) (safely-handle-nondebug-protocol protocol) (loop (gds-read)))) (lambda ignored #f))) (define (safely-handle-nondebug-protocol protocol) ;; This catch covers any internal errors in the GDS code or ;; protocol. (catch #t (lambda () (lazy-catch #t (lambda () (handle-nondebug-protocol protocol)) save-lazy-trap-context-and-rethrow)) (lambda (key . args) (write-form `(eval-results (error . ,(format #f "~s" protocol)) ,(if last-lazy-trap-context 't 'nil) "GDS Internal Error Please report this to , ideally including: - a description of the scenario in which this error occurred - which versions of Guile and guile-debugging you are using - the error stack, which you can get by clicking on the link below, and then cut and paste into your report. Thanks!\n\n" ,(list (with-output-to-string (lambda () (write key) (display ": ") (write args) (newline))))))))) ;; The key that is used to signal a read error changes from 1.6 to ;; 1.8; here we cover all eventualities by discovering the key ;; dynamically. (define read-error-key (catch #t (lambda () (with-input-from-string "(+ 3 4" read)) (lambda (key . args) key))) (define (handle-nondebug-protocol protocol) (case (car protocol) ((eval) (set! last-lazy-trap-context #f) (apply (lambda (correlator module port-name line column code) (with-input-from-string code (lambda () (set-port-filename! (current-input-port) port-name) (set-port-line! (current-input-port) line) (set-port-column! (current-input-port) column) (let ((m (and module (resolve-module-from-root module)))) (catch read-error-key (lambda () (let loop ((exprs '()) (x (read))) (if (eof-object? x) ;; Expressions to be evaluated have all ;; been read. Now evaluate them. (let loop2 ((exprs (reverse! exprs)) (results '()) (n 1)) (if (null? exprs) (write-form `(eval-results ,correlator ,(if last-lazy-trap-context 't 'nil) ,@results)) (loop2 (cdr exprs) (append results (gds-eval (car exprs) m (if (and (null? (cdr exprs)) (= n 1)) #f n))) (+ n 1)))) ;; Another complete expression read; add ;; it to the list. (begin (for-each-breakpoint setup-after-read x) (loop (cons x exprs) (read)))))) (lambda (key . args) (write-form `(eval-results ,correlator ,(if last-lazy-trap-context 't 'nil) ,(with-output-to-string (lambda () (display ";;; Reading expressions") (display " to evaluate\n") (apply display-error #f (current-output-port) args))) ("error-in-read")))))))) (if (string? port-name) (without-traps (lambda () (for-each-breakpoint setup-after-eval port-name))))) (cdr protocol))) ((complete) (let ((matches (apropos-internal (string-append "^" (regexp-quote (cadr protocol)))))) (cond ((null? matches) (write-form '(completion-result nil))) (else ;;(write matches (current-error-port)) ;;(newline (current-error-port)) (let ((match (let loop ((match (symbol->string (car matches))) (matches (cdr matches))) ;;(write match (current-error-port)) ;;(newline (current-error-port)) ;;(write matches (current-error-port)) ;;(newline (current-error-port)) (if (null? matches) match (if (string-prefix=? match (symbol->string (car matches))) (loop match (cdr matches)) (loop (substring match 0 (- (string-length match) 1)) matches)))))) (if (string=? match (cadr protocol)) (write-form `(completion-result ,(map symbol->string matches))) (write-form `(completion-result ,match)))))))) ((debug-lazy-trap-context) (if last-lazy-trap-context (gds-debug-trap last-lazy-trap-context) (error "There is no stack available to show"))) ((set-breakpoint) ;; Create or update a breakpoint object according to the ;; definition. If the target code is already loaded, note that ;; this may immediately install a trap. (let* ((num (cadr protocol)) (def (caddr protocol)) (behaviour (case (list-ref def 0) ((debug) gds-debug-trap) ((trace) gds-trace-trap) ((trace-subtree) gds-trace-subtree) (else (error "Unsupported behaviour:" (list-ref def 0))))) (bp (hash-ref breakpoints num))) (trc 'existing-bp bp) (if bp (update-breakpoint bp (list-ref def 3)) (begin (set! bp (case (list-ref def 1) ((in) (break-in (string->symbol (list-ref def 3)) (list-ref def 2) #:behaviour behaviour)) ((at) (break-at (list-ref def 2) (car (list-ref def 3)) (cdr (list-ref def 3)) #:behaviour behaviour)) (else (error "Unsupported breakpoint type:" (list-ref def 1))))) ;; Install an observer that will tell the frontend about ;; future changes in this breakpoint's status. (slot-set! bp 'observer (lambda () (write-form `(breakpoint ,num ,@(map trap-description (slot-ref bp 'traps)))))) ;; Add this to the breakpoint hash, and return the ;; breakpoint number and status to the front end. (hash-set! breakpoints num bp))) ;; Call the breakpoint's observer now. ((slot-ref bp 'observer)))) ((delete-breakpoint) (let* ((num (cadr protocol)) (bp (hash-ref breakpoints num))) (if bp (begin (hash-remove! breakpoints num) (delete-breakpoint bp))))) ;;; ((describe-breakpoints) ;;; ;; Describe all breakpoints. ;;; (let ((desc ;;; (with-output-to-string ;;; (lambda () ;;; (hash-fold (lambda (num bp acc) ;;; (format #t ;;; "Breakpoint ~a ~a (~a):\n" ;;; (class-name (class-of bp)) ;;; num ;;; (slot-ref bp 'status)) ;;; (for-each (lambda (trap) ;;; (write (trap-description trap)) ;;; (newline)) ;;; (slot-ref bp 'traps))) ;;; #f ;;; breakpoints))))) ;;; (write-form (list 'info-result desc)))) (else (error "Unexpected protocol:" protocol)))) (define breakpoints (make-hash-table 11)) (define (resolve-module-from-root name) (save-module-excursion (lambda () (set-current-module the-root-module) (resolve-module name)))) (define (gds-eval x m part) ;; Consumer to accept possibly multiple values and present them for ;; Emacs as a list of strings. (define (value-consumer . values) (if (unspecified? (car values)) '() (map (lambda (value) (with-output-to-string (lambda () (write value)))) values))) ;; Now do evaluation. (let ((intro (if part (format #f ";;; Evaluating expression ~A" part) ";;; Evaluating")) (value #f)) (let* ((do-eval (if m (lambda () (display intro) (display " in module ") (write (module-name m)) (newline) (set! value (call-with-values (lambda () (start-stack 'gds-eval-stack (eval x m))) value-consumer))) (lambda () (display intro) (display " in current module ") (write (module-name (current-module))) (newline) (set! value (call-with-values (lambda () (start-stack 'gds-eval-stack (primitive-eval x))) value-consumer))))) (output (with-output-to-string (lambda () (catch #t (lambda () (lazy-catch #t do-eval save-lazy-trap-context-and-rethrow)) (lambda (key . args) (case key ((misc-error signal unbound-variable numerical-overflow) (apply display-error #f (current-output-port) args) (set! value '("error-in-evaluation"))) (else (display "EXCEPTION: ") (display key) (display " ") (write args) (newline) (set! value '("unhandled-exception-in-evaluation")))))))))) (list output value)))) (define last-lazy-trap-context #f) (define (save-lazy-trap-context-and-rethrow key . args) (set! last-lazy-trap-context (throw->trap-context key args save-lazy-trap-context-and-rethrow)) (apply throw key args)) (define (run-utility) (set-gds-breakpoints) (write (getpid)) (newline) (force-output) (named-module-use! '(guile-user) '(ice-9 session)) (gds-accept-input #f)) (define (set-gds-breakpoints) (connect-to-gds) (write-form '(get-breakpoints)) (gds-accept-input #t)) (define-method (trap-description (trap )) (let loop ((description (list (class-name (class-of trap)))) (next 'installed?)) (case next ((installed?) (loop (if (slot-ref trap 'installed) (cons 'installed description) description) 'conditional?)) ((conditional?) (loop (if (slot-ref trap 'condition) (cons 'conditional description) description) 'skip-count)) ((skip-count) (loop (let ((skip-count (slot-ref trap 'skip-count))) (if (zero? skip-count) description (cons* skip-count 'skip-count description))) 'single-shot?)) ((single-shot?) (loop (if (slot-ref trap 'single-shot) (cons 'single-shot description) description) 'done)) (else (reverse! description))))) (define-method (trap-description (trap )) (let ((description (next-method))) (set-cdr! description (cons (procedure-name (slot-ref trap 'procedure)) (cdr description))) description)) (define-method (trap-description (trap )) (let ((description (next-method))) (set-cdr! description (cons (format #f "~s" (slot-ref trap 'expression)) (cdr description))) description)) (define-method (trap-description (trap )) (let ((description (next-method))) (set-cdr! description (cons* (slot-ref trap 'file-regexp) (slot-ref trap 'line) (slot-ref trap 'column) (cdr description))) description)) (define (gds-trace-trap trap-context) (connect-to-gds) (gds-do-trace trap-context) (at-exit (tc:depth trap-context) gds-do-trace)) (define (gds-do-trace trap-context) (write-form (list 'trace (format #f "~3@a: ~a" (trace/stack-real-depth trap-context) (trace/info trap-context))))) (define (gds-trace-subtree trap-context) (connect-to-gds) (gds-do-trace trap-context) (let ((step-trap (make #:behaviour gds-do-trace))) (install-trap step-trap) (at-exit (tc:depth trap-context) (lambda (trap-context) (uninstall-trap step-trap))))) ;;; (ice-9 gds-client) ends here.