Skip to content

Commit 3ae9101

Browse files
committed
Merge branch 'read-timeouts'
2 parents b9d2a03 + 9a35912 commit 3ae9101

File tree

5 files changed

+86
-17
lines changed

5 files changed

+86
-17
lines changed

src/clj_libssh2/channel.clj

+33-12
Original file line numberDiff line numberDiff line change
@@ -398,20 +398,40 @@
398398
[session channel stream]
399399
(if (not= :eof (:status stream))
400400
(let [pump-fn (if (= :output (:direction stream)) pull push)
401-
last-read-time (:last-read-time stream)
402-
new-status (pump-fn session channel (:id stream) (:stream stream))
403-
now (System/currentTimeMillis)]
404-
(when (and (= pump-fn pull)
405-
(= :eagain new-status)
406-
(< (-> session :options :read-timeout) (- now last-read-time)))
407-
(error/raise "Read timeout on a channel."
408-
{:direction (-> stream :direction name)
409-
:id (-> stream :id)
410-
:timeout (-> session :options :read-timeout)
411-
:session session}))
412-
(assoc stream :status new-status :last-read-time now))
401+
new-status (pump-fn session channel (:id stream) (:stream stream))]
402+
(assoc stream :status new-status
403+
:last-read-time (if (= :ready new-status)
404+
(System/currentTimeMillis)
405+
(:last-read-time stream))))
413406
stream))
414407

408+
(defn- enforce-read-timeout
409+
"Enforce the read timeout on the output streams in a set of streams.
410+
411+
Arguments:
412+
413+
session The clj-libssh2.session.Session object for the current session.
414+
channel The SSH channel that we're enforcing timeouts on.
415+
streams The collection of streams that are in use in pump
416+
417+
Return:
418+
419+
nil, or throw an exception if the timeout is exceeded on any of the streams
420+
given."
421+
[session channel streams]
422+
(let [read-timeout (-> session :options :read-timeout)
423+
last-read-time (->> streams
424+
(remove #(= :input (:direction %)))
425+
(map :last-read-time)
426+
(#(when-not (empty? %)
427+
(apply max %))))]
428+
(when (and (some? last-read-time)
429+
(< read-timeout (- (System/currentTimeMillis) last-read-time)))
430+
(error/raise "Read timeout on a channel."
431+
{:timeout read-timeout
432+
:session session
433+
:channel channel}))))
434+
415435
(defn pump
416436
"Process a collection of input and output streams all at once. This will run
417437
until all streams have reported EOF.
@@ -452,6 +472,7 @@
452472
(do
453473
(when (contains? status-set :eagain)
454474
(wait session))
475+
(enforce-read-timeout session channel s)
455476
(recur (map (partial pump-stream session channel) streams)))
456477
(->> s
457478
(filter #(= :output (:direction %)))

src/clj_libssh2/session.clj

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
(def default-opts
1919
"The default options for a session. These are not only the defaults, but an
2020
exhaustive list of the legal options."
21-
{:character-set "UTF-8"
21+
{:blocking-timeout 60000
22+
:character-set "UTF-8"
2223
:fail-if-not-in-known-hosts false
2324
:fail-unless-known-hosts-matches true
2425
:known-hosts-file nil

src/clj_libssh2/socket.clj

+9-1
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,21 @@
9595
(when (>= 0 select-result)
9696
(handle-errors session libssh2/ERROR_TIMEOUT))))))
9797

98+
(defn enforce-blocking-timeout
99+
[session start-time]
100+
(when (< (-> session :options :blocking-timeout)
101+
(- (System/currentTimeMillis) start-time))
102+
(handle-errors session libssh2/ERROR_TIMEOUT)))
103+
98104
(defmacro block
99105
"Turn a non-blocking call that returns EAGAIN into a blocking one."
100106
[session & body]
101107
`(let [session# ~session
102108
start-time# (System/currentTimeMillis)]
103109
(while (= libssh2/ERROR_EAGAIN (do ~@body))
104110
(handle-errors session#
105-
(wait session# start-time#)))))
111+
(wait session# start-time#))
112+
(enforce-blocking-timeout session# start-time#))))
106113

107114
(defmacro block-return
108115
"Similar to block, but for functions that return a pointer"
@@ -115,5 +122,6 @@
115122
(handle-errors session# errno#)
116123
(when (= libssh2/ERROR_EAGAIN errno#)
117124
(wait session# start-time#))
125+
(enforce-blocking-timeout session# start-time#)
118126
(recur (do ~@body)))
119127
result#))))

test/clj_libssh2/test_ssh.clj

+32-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
[clojure.string :as str]
44
[clojure.test :refer :all]
55
[clj-libssh2.ssh :as ssh]
6-
[clj-libssh2.test-utils :as test]))
6+
[clj-libssh2.test-utils :as test])
7+
(:import [java.io OutputStream]))
78

89
(test/fixtures)
910

@@ -86,7 +87,36 @@
8687
(deftest exec-times-out-when-commands-take-too-long
8788
(testing "Commands that take too long result in a timeout"
8889
(is (thrown? Exception (ssh/exec {:port 2222 :read-timeout 500}
89-
"echo foo; sleep 1; echo bar")))))
90+
"echo foo; sleep 1; echo bar"))))
91+
(testing "Commands that are blocking on input time out correctly"
92+
(test/with-temp-file tempfile
93+
(let [output (atom [])
94+
streaming-reader (proxy [OutputStream] []
95+
(write [b off len]
96+
(swap! output conj (String. b off len))))
97+
run-exec (future
98+
(try
99+
(ssh/exec {:port 2222
100+
:read-timeout 5000}
101+
(str "tail -F " tempfile)
102+
:out streaming-reader)
103+
(catch Throwable t t)))]
104+
; Output starts off empty
105+
(is (empty? @output))
106+
107+
; We put some content into the file we're tailing.
108+
(spit tempfile "Here is some output!\n" :append true)
109+
110+
; We wait for it to turn up on the far side.
111+
(let [start-time (System/currentTimeMillis)]
112+
(while (and (empty? @output)
113+
(> 5000 (- (System/currentTimeMillis) start-time)))
114+
(Thread/sleep 10)))
115+
116+
; Now there should be output and (once the exec finishes) an exception.
117+
(is (= ["Here is some output!\n"] @output))
118+
(is (instance? Throwable @run-exec))
119+
(is (= "Timed out." (:error (ex-data @run-exec))))))))
90120

91121
(deftest scp-from-can-copy-files
92122
(testing "scp-from can copy files from the remote host"

test/clj_libssh2/test_utils.clj

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
[clojure.string :as str]
44
[clojure.test :as test]
55
[net.n01se.clojure-jna :as jna]
6-
[clj-libssh2.logging :as logging]))
6+
[clj-libssh2.logging :as logging])
7+
(:import [java.io File]))
78

89
(def ssh-host "127.0.0.1")
910
(def ssh-port 2222)
@@ -61,3 +62,11 @@
6162
(test/use-fixtures :once (test/join-fixtures
6263
[with-sandbox-sshd
6364
with-really-verbose-logging])))
65+
66+
(defmacro with-temp-file
67+
[file & body]
68+
`(let [file# (File/createTempFile "clj-libssh2" nil)
69+
~file (.getPath file#)]
70+
(try
71+
(do ~@body)
72+
(finally (.delete file#)))))

0 commit comments

Comments
 (0)