Skip to content

Commit 9a35912

Browse files
committed
Enforce read timeouts better.
Ensure that we still time out even if the other end is still working fine, just not producing any output.
1 parent b9d2a03 commit 9a35912

File tree

5 files changed

+86
-17
lines changed

5 files changed

+86
-17
lines changed

src/clj_libssh2/channel.clj

Lines changed: 33 additions & 12 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 9 additions & 1 deletion
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

Lines changed: 32 additions & 2 deletions
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

Lines changed: 10 additions & 1 deletion
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)