165 lines
6.3 KiB
Racket
165 lines
6.3 KiB
Racket
#lang racket/base
|
|
|
|
(require racket/file
|
|
racket/list
|
|
racket/match
|
|
racket/port
|
|
racket/string)
|
|
|
|
(provide js-engine?
|
|
js-engine-name
|
|
js-engine-path
|
|
js-engine-kind
|
|
js-engine-version
|
|
js-run-result?
|
|
js-run-result-engine
|
|
js-run-result-status
|
|
js-run-result-stdout
|
|
js-run-result-stderr
|
|
js-run-result-success?
|
|
known-js-engine-names
|
|
find-js-engine
|
|
run-js-file)
|
|
|
|
(struct js-engine (name path kind version-args) #:transparent)
|
|
(struct js-run-result (engine status stdout stderr) #:transparent)
|
|
|
|
(define (path-string/non-empty? v)
|
|
(and (string? v) (not (string=? v ""))))
|
|
|
|
(define (executable-file? p)
|
|
(and p (file-exists? p)))
|
|
|
|
(define (getenv/path name)
|
|
(define v (getenv name))
|
|
(and (path-string/non-empty? v) (string->path v)))
|
|
|
|
(define engine-specs
|
|
;; name aliases kind version-args extra-paths
|
|
'((node ("node" "nodejs") plain ("--version")
|
|
("/opt/nvm/versions/node/v22.16.0/bin/node" "/usr/local/bin/node" "/usr/bin/node"))
|
|
(deno ("deno") plain ("--version") ())
|
|
(bun ("bun") plain ("--version") ())
|
|
(qjs ("qjs" "quickjs") plain ("-v") ())
|
|
(d8 ("d8") plain ("--version") ())
|
|
(jsc ("jsc") plain ("--version") ())
|
|
(js ("js") plain ("--version") ())
|
|
(chromium ("chromium" "chromium-browser" "google-chrome" "google-chrome-stable") chromium ("--version")
|
|
("/usr/bin/chromium" "/usr/bin/chromium-browser" "/usr/bin/google-chrome"))))
|
|
|
|
(define (known-js-engine-names)
|
|
(map car engine-specs))
|
|
|
|
(define (lookup-engine-spec name)
|
|
(define wanted (string-downcase (format "~a" name)))
|
|
(for/first ([spec (in-list engine-specs)]
|
|
#:when (or (string=? wanted (symbol->string (car spec)))
|
|
(member wanted (cadr spec))))
|
|
spec))
|
|
|
|
(define (candidate-paths spec)
|
|
(match spec
|
|
[(list name aliases kind version-args extra-paths)
|
|
(append
|
|
(if (eq? name 'node)
|
|
(filter values (list (getenv/path "JSMAKER_NODE")))
|
|
'())
|
|
(filter values (list (getenv/path "JSMAKER_ENGINE_PATH")))
|
|
(filter values (map find-executable-path aliases))
|
|
(map string->path extra-paths))]))
|
|
|
|
(define (make-engine-from-spec spec)
|
|
(match spec
|
|
[(list name aliases kind version-args extra-paths)
|
|
(for/first ([p (in-list (candidate-paths spec))]
|
|
#:when (executable-file? p))
|
|
(js-engine name p kind version-args))]))
|
|
|
|
(define (find-js-engine [requested (or (getenv "JSMAKER_ENGINE") "auto")])
|
|
(define req (string-downcase requested))
|
|
(cond
|
|
[(or (string=? req "") (string=? req "auto"))
|
|
(define browser-fallback? (getenv "JSMAKER_BROWSER_FALLBACK"))
|
|
(or (for*/first ([spec (in-list engine-specs)]
|
|
#:unless (and (not browser-fallback?) (eq? (car spec) 'chromium))
|
|
[engine (in-value (make-engine-from-spec spec))]
|
|
#:when engine)
|
|
engine)
|
|
#f)]
|
|
[else
|
|
(define spec (lookup-engine-spec req))
|
|
(and spec (make-engine-from-spec spec))]))
|
|
|
|
(define (capture-timeout-seconds)
|
|
(define v (getenv "JSMAKER_ENGINE_TIMEOUT_SECONDS"))
|
|
(cond
|
|
[(and (path-string/non-empty? v) (string->number v)) => values]
|
|
[else 15]))
|
|
|
|
(define (capture path args)
|
|
(with-handlers ([exn:fail? (lambda (e) (values 127 "" (exn-message e)))])
|
|
(define-values (proc stdout stdin stderr)
|
|
(apply subprocess #f #f #f path args))
|
|
(define waiter (thread (lambda () (subprocess-wait proc))))
|
|
(define finished? (sync/timeout (capture-timeout-seconds) waiter))
|
|
(unless finished?
|
|
(subprocess-kill proc #t)
|
|
(thread-wait waiter))
|
|
(values (if finished? (subprocess-status proc) 124)
|
|
(port->string stdout)
|
|
(string-append (port->string stderr)
|
|
(if finished? "" "\nJavaScript engine timed out and was killed.\n")))))
|
|
|
|
(define (js-engine-version engine)
|
|
(define-values (status stdout stderr)
|
|
(capture (js-engine-path engine) (js-engine-version-args engine)))
|
|
(define s (string-trim (if (string=? stdout "") stderr stdout)))
|
|
(and (zero? status) (not (string=? s "")) s))
|
|
|
|
(define (plain-run-args engine js-path)
|
|
(case (js-engine-name engine)
|
|
[(deno) (list "run" "--quiet" js-path)]
|
|
[else (list js-path)]))
|
|
|
|
(define (copy-js-as-browser-html js-path)
|
|
(define html-path (build-path (find-system-path 'temp-dir)
|
|
(format "jsmaker-browser-~a.html" (current-inexact-milliseconds))))
|
|
(define js (file->string js-path))
|
|
(call-with-output-file html-path
|
|
#:exists 'replace
|
|
(lambda (out)
|
|
(displayln "<!doctype html><meta charset=\"utf-8\"><pre id=\"out\"></pre>" out)
|
|
(displayln "<script>" out)
|
|
(displayln "const __out = document.getElementById('out');" out)
|
|
(displayln "const print = (...xs) => { __out.append(xs.join(' ') + '\\n'); };" out)
|
|
(displayln "console.log = (...xs) => print(...xs);" out)
|
|
(displayln "console.error = (...xs) => print(...xs);" out)
|
|
(displayln "try {" out)
|
|
(display js out)
|
|
(displayln "\ndocument.documentElement.setAttribute('data-jsmaker-status', 'ok');" out)
|
|
(displayln "} catch (e) {" out)
|
|
(displayln " print('ERROR\\t' + (e && e.stack ? e.stack : String(e)));" out)
|
|
(displayln " document.documentElement.setAttribute('data-jsmaker-status', 'fail');" out)
|
|
(displayln "}" out)
|
|
(displayln "</script>" out)))
|
|
html-path)
|
|
|
|
(define (chromium-run engine js-path)
|
|
(define html-path (copy-js-as-browser-html js-path))
|
|
(define url (string-append "file://" (path->string html-path)))
|
|
(define args (list "--headless" "--disable-gpu" "--no-sandbox" "--dump-dom" url))
|
|
(define-values (status stdout stderr) (capture (js-engine-path engine) args))
|
|
(define browser-fail? (regexp-match? #rx"data-jsmaker-status=\"fail\"" stdout))
|
|
(js-run-result engine (if browser-fail? 1 status) stdout stderr))
|
|
|
|
(define (run-js-file engine js-path)
|
|
(case (js-engine-kind engine)
|
|
[(chromium) (chromium-run engine js-path)]
|
|
[else
|
|
(define-values (status stdout stderr)
|
|
(capture (js-engine-path engine) (plain-run-args engine js-path)))
|
|
(js-run-result engine status stdout stderr)]))
|
|
|
|
(define (js-run-result-success? result)
|
|
(zero? (js-run-result-status result)))
|