Skip to content

Commit 7f31fea

Browse files
committed
Minimal safari driver logging
Safaridriver logging is relatively odd. It only has debug level logging, only logs to a file, and discovering that file is somewhat tricky. This change adds minimal support to discover and dump safaridriver logs. I'm not sure how many folks bother with safaridriver testing, so this change might be mostly a favour to myself when diagnosing safaridriver issues that arise from time to time. Closes #563 Incidentally fixes #517
1 parent da66c37 commit 7f31fea

File tree

9 files changed

+359
-37
lines changed

9 files changed

+359
-37
lines changed

CHANGELOG.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ A release with an intentional breaking changes is marked with:
1010
* https://github.com/clj-commons/etaoin/pull/552[#552]: Add support for wide characters to input fill functions
1111
(https://github.com/tupini07[@tupini07])
1212
* https://github.com/clj-commons/etaoin/issues/566[#566]: Recognize `:driver-log-level` for Edge
13+
* https://github.com/clj-commons/etaoin/issues/563[#563]: Support `"debug"` `:driver-log-level` for Safari
14+
* https://github.com/clj-commons/etaoin/issues/517[#517]: Properly cleanup after failed webdriver launch
1315
* bump all deps to current versions
1416
* tests
1517
** https://github.com/clj-commons/etaoin/issues/572[#572]: stop using chrome `--no-sandbox` option, it has become problematic on Windows (and we did not need it anyway)

bb.edn

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@
2222
:default "localhost"}}
2323
:restrict true}]
2424
(cli/parse-opts args cli-spec))))
25-
:enter (let [{:keys [name]} (current-task)]
26-
(when-not (string/starts-with? name "-")
27-
(status/line :head "TASK %s %s" name (string/join " " *command-line-args*))))
28-
:leave (let [{:keys [name]} (current-task)]
29-
(when-not (string/starts-with? name "-")
25+
:enter (let [{:keys [name task-decoration] :as f} (current-task)]
26+
(when-not (= :none task-decoration)
27+
(status/line :head "TASK %s" name)))
28+
:leave (let [{:keys [name task-decoration] :as f} (current-task)]
29+
(when-not (= :none task-decoration)
3030
(status/line :detail "\nTASK %s done." name)))
31-
3231
;; commands
3332
dev:jvm {:doc "start a Clojure nrepl server/prompt"
3433
:task (let [opts (parse-repl-args *command-line-args*)
@@ -47,6 +46,7 @@
4746
test:jvm {:doc "Runs tests under JVM Clojure [--help]"
4847
:task test/test-jvm}
4948
-test:bb {:doc "bb test runner, invoked within script/test.clj"
49+
:task-decoration :none
5050
:requires ([taoensso.timbre :as timbre])
5151
;; repeat :test paths from deps.edn
5252
:extra-paths ["test" "env/test/resources"]
@@ -98,6 +98,7 @@
9898
docker-build {:doc "build etaoin docker image"
9999
:task (shell/command "docker build --no-cache -t etaoin:latest .")}
100100
-docker-install {:doc "helper to setup docker image"
101+
:task-decoration :none
101102
:task docker-install/-main}
102103
docker-run {:doc "run etaoin docker image (specify no commmands for interactive)"
103104
:task (let [{:keys [exit]} (apply shell/command {:continue true}
@@ -106,5 +107,8 @@
106107
"etaoin:latest"
107108
*command-line-args*)]
108109
(System/exit exit))}
110+
fake-driver {:doc "Fake driver to support testing"
111+
:task-decoration :none
112+
:task fake-driver/-main}
109113
ci-release {:doc "release tasks, use --help for args"
110114
:task ci-release/-main}}}

doc/01-user-guide.adoc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2196,6 +2196,11 @@ values vary by browser driver vendor:
21962196

21972197
* chrome & edge `"OFF"` `"SEVERE"` `"WARNING"` `"INFO"` or `"DEBUG"`
21982198
* firefox `"fatal"` `"error"` `"warn"` `"info"` `"config"` `"debug"` or `"trace"`
2199+
* safari `"debug"` - safaridriver
2200+
** has only one detailed log level which we enable via its `--diagnose` option and abstract via `"debug"`
2201+
** only logs to a log file which Etaion automatically discovers and populates as <<opt-driver-log-file>> in the `driver` map
2202+
** see <<opt-post-stop-fns>> for one way to dump this log file
2203+
* phantomjs "ERROR"` `"WARN"` `"INFO"` `"DEBUG"`
21992204

22002205
[id=opt-log-stdout,reftext=`:log-stdout`]
22012206
[id=opt-log-stderr,reftext=`:log-sterr`]
@@ -2212,6 +2217,36 @@ _Example:_
22122217

22132218
Specify `:inherit` to have WebDriver process output destination inherit from its calling process (for example, the console or some existing redirection to a file).
22142219

2220+
[id=opt-driver-log-file,reftext=`:driver-log-file`]
2221+
=== `:driver-log-file` for discovered WebDriver log file
2222+
2223+
_Default:_ `<not set>` Etaoin will set this value for you
2224+
2225+
_Example:_ `<n/a>` Not set by user
2226+
2227+
Only populated for safaridriver when <<opt-driver-log-level>> set to `"debug"`.
2228+
2229+
[id=opt-post-stop-fns,reftext=`:post-stop-fns`]
2230+
=== `:post-stop-fns` to hook in behaviour at driver stop
2231+
2232+
_Default:_ `<not set>`
2233+
2234+
_Example:_
2235+
One usage is to dump safaridriver <<opt-driver-log-file>>.
2236+
2237+
//:test-doc-blocks/skip
2238+
[source,clojure]
2239+
----
2240+
:post-stop-fns [(fn dump-discovered-log [driver]
2241+
(if-let [log (:driver-log-file driver)]
2242+
(do
2243+
(println "-[start]-safaridriver log file" log)
2244+
(with-open [in (io/input-stream log)]
2245+
(io/copy in *out*))
2246+
(println "-[end]-safaridriver log file" log))
2247+
(println "-no safaridriver log file discovered-")))]
2248+
----
2249+
22152250
[id=opt-profile,reftext=`:profile`]
22162251
=== `:profile` path to web browser profile
22172252

script/fake_driver.clj

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
(ns fake-driver
2+
"A fake WebDriver to support testing, adapt as necessary for tests"
3+
(:require [babashka.cli :as cli]
4+
[cheshire.core :as json]
5+
[lread.status-line :as status]
6+
[org.httpkit.server :as server])
7+
(:import [sun.misc Signal SignalHandler]))
8+
9+
(def cli-spec {:help {:desc "This usage help" :alias :h}
10+
:port {:ref "<port>"
11+
:desc "Expose server on this port"
12+
:coerce :int
13+
:default 8888
14+
:alias :p}
15+
:sigterm-filename {:ref "<filename>"
16+
:desc "Log a line to this filename when quit via sig term"}
17+
:start-server {:ref "<boolean>"
18+
:desc "Start a fake webdriver request handler"
19+
:default true}})
20+
21+
(defn- usage-help []
22+
(status/line :head "Usage help")
23+
(status/line :detail (cli/format-opts {:spec cli-spec :order [:port :sigterm-filename :start-server :help]})) )
24+
25+
(defn- usage-fail [msg]
26+
(status/line :error msg)
27+
(usage-help)
28+
(System/exit 1))
29+
30+
(defn signal-handler [signal-id handler-fn]
31+
(let [signal (Signal. signal-id)
32+
handler (reify SignalHandler
33+
(handle [_ _] (handler-fn)))]
34+
(Signal/handle signal handler)))
35+
36+
(defn make-handler [_opts]
37+
(fn handle-request [{:keys [request-method uri] :as _req}]
38+
(cond
39+
(and (= :post request-method) (= "/session" uri))
40+
{:status 200
41+
:headers {"Content-Type" "application/json"}
42+
:body (json/generate-string {:sessionId (random-uuid)})}
43+
(and (= :get request-method) (= "/status" uri))
44+
{:status 200
45+
:headers {"Content-Type" "application/json"}
46+
:body (json/generate-string {:ready true})}
47+
:else
48+
{:status 404})))
49+
50+
(defn -main [& args]
51+
(let [opts (cli/parse-opts args {:spec cli-spec
52+
:restrict true
53+
:error-fn (fn [{:keys [msg]}]
54+
(usage-fail msg))})]
55+
(if (:help opts)
56+
(usage-help)
57+
(do
58+
(when-let [log (:sigterm-filename opts)]
59+
(signal-handler "TERM" (fn sigterm-handler []
60+
;; I don't think we need to worry about concurrency for our use case
61+
(spit log "SIGTERM received, quitting" :append true)
62+
(System/exit 0))))
63+
64+
(when (:start-server opts)
65+
(server/run-server (make-handler opts) opts)))))
66+
67+
@(promise))

src/etaoin/api.clj

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
"
145145
(:require
146146
[babashka.fs :as fs]
147+
[babashka.process :as p]
147148
[cheshire.core :as json]
148149
[clojure.java.io :as io]
149150
[clojure.string :as str]
@@ -2475,7 +2476,7 @@
24752476

24762477
(defn wait-predicate
24772478
"Wakes up every `:interval` seconds to call `pred`.
2478-
Keeps this up until either `pred` returns true or `:timeout` has elapsed.
2479+
Keeps this up until either `pred` returns truthy or `:timeout` has elapsed.
24792480
When `:timeout` has elapsed a slingshot exception is throws with `:message`.
24802481
24812482
Arguments:
@@ -2501,13 +2502,15 @@
25012502
:interval interval
25022503
:times times
25032504
:predicate pred}))
2504-
(when-not (with-http-error
2505-
(pred))
2506-
(wait interval)
2507-
(recur pred (assoc
2508-
opts
2509-
:time-rest (- time-rest interval)
2510-
:times (inc times)))))))
2505+
(let [res (with-http-error
2506+
(pred))]
2507+
(or res
2508+
(do
2509+
(wait interval)
2510+
(recur pred (assoc
2511+
opts
2512+
:time-rest (- time-rest interval)
2513+
:times (inc times)))))))))
25112514

25122515
(defn wait-exists
25132516
"Waits until `driver` finds element [[exists?]] via `q`.
@@ -3440,6 +3443,54 @@
34403443
http (assoc :http http)
34413444
ssl (assoc :ssl ssl))))
34423445

3446+
(defn- discover-safari-webdriver-log-pid [port]
3447+
;; The safaridriver docs mislead, the pid in the filename is not the pid of safaridriver,
3448+
;; it is of com.apple.WebDriver.HTTPService get the pid of whatever is listening to
3449+
;; specified safaridriver port, this is macOS specific, but so is safari
3450+
(let [{:keys [exit out]} (p/shell {:out :string} "lsof -ti" (str ":" port))]
3451+
(when (zero? exit)
3452+
(str/trim out))))
3453+
3454+
(defn- discover-safari-webdriver-log [{:keys [port created-epoch-ms] :as driver}]
3455+
(let [pid (wait-predicate #(discover-safari-webdriver-log-pid port)
3456+
{:timeout 0
3457+
:interval 0.2
3458+
:message (format "Cannot discover safaridriver log file pid for port %s" port)})]
3459+
;; force some output so that log file is created
3460+
(get-status driver)
3461+
(let [dir (fs/file (fs/home) "Library/Logs/com.apple.WebDriver")
3462+
glob (format "safaridriver.%s.*.txt" pid)
3463+
log-files (->> (fs/glob dir glob)
3464+
;; use last modified instead of fs/creation-time it is more reliable
3465+
;; creation-time was returning time in second resolution, not millisecond resolution
3466+
(filter #(>= (-> % fs/last-modified-time fs/file-time->millis)
3467+
created-epoch-ms))
3468+
(sort-by #(fs/last-modified-time %))
3469+
(mapv str))]
3470+
(cond
3471+
(zero? (count log-files))
3472+
(log/warnf "Safaridriver log file not found for pid %s." pid)
3473+
3474+
(not= 1 (count log-files))
3475+
(let [candidates (->> log-files
3476+
(mapv #(format " %s %s"
3477+
(-> % fs/last-modified-time fs/file-time->instant)
3478+
%)))]
3479+
(log/warnf "Found multiple matching safaridriver log file candidates, assuming latest from:\n%s"
3480+
(str/join "\n" candidates))))
3481+
(if-let [log-file (last log-files)]
3482+
(do (log/infof "Safaridriver log file discovered %s" log-file)
3483+
(assoc driver :driver-log-file log-file))
3484+
driver))))
3485+
3486+
(defn stop-driver
3487+
"Returns new `driver` after killing its WebDriver process."
3488+
[driver]
3489+
(proc/kill (:process driver))
3490+
(doseq [f (:post-stop-fns driver)]
3491+
(f driver))
3492+
(dissoc driver :process :args :env :capabilities :pre-stop-fns))
3493+
34433494
(defn- -run-driver*
34443495
[driver & [{:keys [dev
34453496
env
@@ -3450,7 +3501,8 @@
34503501
path-driver
34513502
download-dir
34523503
path-browser
3453-
driver-log-level]}]]
3504+
driver-log-level
3505+
post-stop-fns]}]]
34543506
(let [{:keys [port host]} driver
34553507

34563508
_ (when (util/connectable? host port)
@@ -3472,7 +3524,16 @@
34723524
process (proc/run proc-args {:log-stdout log-stdout
34733525
:log-stderr log-stderr
34743526
:env env})]
3475-
(util/assoc-some driver :env env :process process)))
3527+
(util/assoc-some driver
3528+
:env env
3529+
:process process
3530+
:post-stop-fns post-stop-fns
3531+
:created-epoch-ms (System/currentTimeMillis))))
3532+
3533+
(defn- driver-action [driver action]
3534+
(case action
3535+
:discover-safari-webdriver-log (discover-safari-webdriver-log driver)
3536+
(throw (ex-info (str "Internal error: unrecognized action " action) {}))))
34763537

34773538
(defn- -run-driver
34783539
"Runs a driver process locally.
@@ -3528,12 +3589,15 @@
35283589
res (try
35293590
(wait-running driver)
35303591
(catch Exception e
3592+
(stop-driver driver)
35313593
{:exception e}))]
35323594
(if (not (:exception res))
3533-
driver
3595+
(reduce (fn [driver action]
3596+
(driver-action driver action))
3597+
driver
3598+
(:post-run-actions driver))
35343599
(recur (inc try-num) (:exception res)))))))))
35353600

3536-
35373601
(defn- -connect-driver
35383602
"Connects to a running Webdriver server.
35393603
@@ -3638,12 +3702,6 @@
36383702

36393703
(dissoc driver :session))
36403704

3641-
(defn stop-driver
3642-
"Returns new `driver` after killing its WebDriver process."
3643-
[driver]
3644-
(proc/kill (:process driver))
3645-
(dissoc driver :process :args :env :capabilities))
3646-
36473705
(defn quit
36483706
"Have `driver` close the current session, then, if Etaoin launched it, kill the WebDriver process."
36493707
[driver]

src/etaoin/impl/driver.clj

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,12 +485,19 @@
485485
[driver log-level]
486486
(set-args driver ["--log" log-level]))
487487

488+
(defmethod set-driver-log-level
489+
:safari
490+
[driver log-level]
491+
(when-not (= "debug" (string/lower-case log-level))
492+
(throw (ex-info "Safari Driver only supports debug level logging" {})))
493+
(-> (set-args driver ["--diagnose"])
494+
(update :post-run-actions (fnil conj []) :discover-safari-webdriver-log)))
495+
488496
(defmethod set-driver-log-level
489497
:phantom
490498
[driver log-level]
491499
(set-args driver [(format "--webdriver-loglevel=%s" log-level)]))
492500

493-
494501
;;
495502
;; User-Agent
496503
;; https://stackoverflow.com/questions/29916054/

test/etaoin/api_test.clj

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,19 @@
5050
:log-stderr :inherit
5151
:driver-log-level "info"}))
5252
:safari (cond-> {}
53-
;; add logging for kind flaky CI scenario (maybe we'll answer why we need
53+
;; add logging for kind of flaky CI scenario (maybe we'll answer why we need
5454
;; to retry launching safaridriver automatically)
55-
;; safaridriver only logs details to a somewhat obscure file, will follow up
56-
;; with some technique to discover/dump this file
57-
(ci?) (merge {:log-stdout :inherit :log-stderr :inherit}))
55+
(ci?) (merge {:log-stdout :inherit
56+
:log-stderr :inherit
57+
:driver-log-level "debug"
58+
:post-stop-fns [(fn dump-discovered-log [driver]
59+
(if-let [log (:driver-log-file driver)]
60+
(do
61+
(println "-[start]-safaridriver log file" log)
62+
(with-open [in (io/input-stream log)]
63+
(io/copy in *out*))
64+
(println "-[end]-safaridriver log file" log))
65+
(println "-no safaridriver log file discovered-")))]}))
5866
:edge {:args ["--headless"]}})
5967

6068
(def drivers

0 commit comments

Comments
 (0)