Skip to content

Commit 31f388c

Browse files
committed
Merge branch 'exec'
2 parents db16f71 + e38b066 commit 31f388c

File tree

10 files changed

+508
-12
lines changed

10 files changed

+508
-12
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,35 @@ parts to this library:
1717

1818
## clj-libssh2 API
1919

20-
TODO: Examples
20+
The primary public API for this library is the following set of functions:
21+
22+
- `clj-libssh2.ssh/exec` - Execute a command on the remote host.
23+
- `clj-libssh2.ssh/with-session` - A convenience macro for managing sessions.
24+
25+
### Examples
26+
27+
#### Run a command and get the output.
28+
29+
```clojure
30+
user=> (require '[clj-libssh2.ssh :as ssh])
31+
nil
32+
user=> (ssh/exec {:hostname "127.0.0.1"} "uptime")
33+
{:out "18:40 up 155 days, 19:04, 4 users, load averages: 2.45 1.76 1.66\n",
34+
:err "", :exit 0, :signal {:exit-signal nil, :err-msg nil, :lang-tag nil}}
35+
```
36+
37+
#### Run multiple commands on the same session
38+
39+
```clojure
40+
user=> (require '[clj-libssh2.ssh :as ssh])
41+
nil
42+
user=> (ssh/with-session session {:hostname "127.0.0.1"}
43+
#_=> (print (:out (ssh/exec session "uptime")))
44+
#_=> (print (:out (ssh/exec session "date"))))
45+
18:44 up 155 days, 19:07, 4 users, load averages: 2.17 1.93 1.75
46+
Sun 17 Jan 2016 18:44:03 GMT
47+
nil
48+
```
2149

2250
## libssh2 API
2351

project.clj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
:url "https://github.com/conormcd/clj-libssh2/blob/master/LICENSE"}
66
:pedantic? :abort
77
:dependencies [[org.clojure/clojure "1.7.0"]
8-
[net.n01se/clojure-jna "1.0.0"]])
8+
[net.n01se/clojure-jna "1.0.0"]]
9+
:jvm-opts ["-Xmx1g"
10+
"-XX:+TieredCompilation"
11+
"-XX:TieredStopAtLevel=1"])
152 Bytes
Binary file not shown.
97 Bytes
Binary file not shown.

src/clj_libssh2/channel.clj

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
(ns clj-libssh2.channel
2+
(:require [net.n01se.clojure-jna :as jna]
3+
[clj-libssh2.error :refer [handle-errors]]
4+
[clj-libssh2.libssh2 :as libssh2]
5+
[clj-libssh2.libssh2.channel :as libssh2-channel]
6+
[clj-libssh2.socket :refer [block block-return wait]])
7+
(:import [java.io InputStream OutputStream PushbackInputStream]
8+
[com.sun.jna.ptr IntByReference PointerByReference]))
9+
10+
(defn close
11+
"Close a channel."
12+
[session channel]
13+
(block session
14+
(handle-errors session (libssh2-channel/close channel))))
15+
16+
(defn exec
17+
"Execute a command via a channel."
18+
[session channel commandline]
19+
(block session
20+
(handle-errors session (libssh2-channel/exec channel commandline))))
21+
22+
(defn exit-signal
23+
"Collect the exit signal data from a channel."
24+
[session channel]
25+
(let [->str (fn [string-ref length-ref]
26+
(let [string (.getValue string-ref)
27+
length (.getValue length-ref)]
28+
(when (< 0 length)
29+
(String. (.getByteArray string 0 length) "ASCII"))))
30+
exit-signal (PointerByReference.)
31+
exit-signal-len (IntByReference.)
32+
err-msg (PointerByReference.)
33+
err-msg-len (IntByReference.)
34+
lang-tag (PointerByReference.)
35+
lang-tag-len (IntByReference.)]
36+
(handle-errors session
37+
(libssh2-channel/get-exit-signal channel
38+
exit-signal exit-signal-len
39+
err-msg err-msg-len
40+
lang-tag lang-tag-len))
41+
{:exit-signal (->str exit-signal exit-signal-len)
42+
:err-msg (->str err-msg err-msg-len)
43+
:lang-tag (->str lang-tag lang-tag-len)}))
44+
45+
(defn exit-status
46+
"Get the exit code from the last executed command."
47+
[channel]
48+
(libssh2-channel/get-exit-status channel))
49+
50+
(defn free
51+
"Free a channel."
52+
[channel]
53+
(libssh2-channel/free channel))
54+
55+
(defn open
56+
"Create a new channel in a session."
57+
[session]
58+
(block-return session
59+
(libssh2-channel/open-session (:session session))))
60+
61+
(defn send-eof
62+
"Tell the remote process that we're done sending input."
63+
[session channel]
64+
(block session (handle-errors session (libssh2-channel/send-eof channel))))
65+
66+
(defn pull
67+
"Read some output from a given stream on a channel.
68+
69+
Parameters:
70+
71+
session The usual session object.
72+
channel A valid channel for this session.
73+
ssh-stream-id 0 for STDOUT, 1 for STDERR or any other number that the
74+
process on the other end wishes to send data on.
75+
output-stream A java.io.OutputStream to send the data to.
76+
77+
Return:
78+
79+
Either :eof, :eagain or :ready. If :eof, then no more data will be sent. If
80+
:eagain, then more data might be available. You should select on the
81+
appropriate socket and try again. If :ready, the stream is ready for another
82+
read immediately."
83+
[session channel ssh-stream-id ^OutputStream output-stream]
84+
(let [size (-> session :options :read-chunk-size)
85+
buf1 (jna/make-cbuf size)
86+
res (handle-errors session
87+
(libssh2-channel/read-ex channel ssh-stream-id buf1 size))]
88+
(when (and (some? output-stream) (< 0 res))
89+
(let [buf2 (byte-array size (byte 0))]
90+
(.get buf1 buf2 0 res)
91+
(.write output-stream buf2 0 res)))
92+
(condp = res
93+
0 :eof
94+
libssh2/ERROR_EAGAIN :eagain
95+
:ready)))
96+
97+
(defn push
98+
"Write some input to a given stream on a channel.
99+
100+
Parameters:
101+
102+
session The usual session object.
103+
channel A valid channel for this session.
104+
ssh-stream-id 0 for STDIN or any other number that the process on the other
105+
end wishes to receive data on.
106+
input-stream A java.io.PushbackInputStream to grab the data from. This must
107+
be capable of pushing back as `:write-chunk-size` bytes.
108+
109+
Return:
110+
111+
Either :eof or :ready. If :eof, then the input-stream has returned -1 and no
112+
more data will be read from it nor written to the channel. If :eagain, then
113+
you should select on the appropriate socket before calling this again. If
114+
:ready then there are more bytes to be processed and this function should be
115+
called again."
116+
[session channel ssh-stream-id ^PushbackInputStream input-stream]
117+
{:pre [(instance? PushbackInputStream input-stream)]}
118+
(let [size (-> session :options :write-chunk-size)
119+
read-size (min size (-> session :options :read-chunk-size))
120+
buf (byte-array read-size (byte 0))
121+
bytes-read (.read input-stream buf)]
122+
(if (< -1 bytes-read)
123+
(if (< 0 bytes-read)
124+
(let [sent (handle-errors session
125+
(libssh2-channel/write-ex channel ssh-stream-id buf size))]
126+
(when (< sent bytes-read)
127+
(let [bytes-sent (if (< 0 sent) sent 0)]
128+
(.unread input-stream buf bytes-sent (- bytes-read bytes-sent))))
129+
(if (= libssh2/ERROR_EAGAIN sent)
130+
:eagain
131+
:ready))
132+
:ready)
133+
(do
134+
(send-eof session channel)
135+
:eof))))
136+
137+
(defn- ensure-pushback
138+
[size stream]
139+
(let [s (:stream stream)]
140+
(if (and (instance? InputStream s) (not (instance? PushbackInputStream s)))
141+
(assoc stream :stream (PushbackInputStream. s size))
142+
stream)))
143+
144+
(defn- make-stream
145+
[now direction [id stream]]
146+
(hash-map :id id
147+
:direction direction
148+
:stream stream
149+
:last-read-time now
150+
:status :ready))
151+
152+
(defn- pump-stream
153+
"Do exactly one push/pull on a stream and enforce read timeouts"
154+
[session channel stream]
155+
(if (not= :eof (:status stream))
156+
(let [pump-fn (if (= :output (:direction stream)) pull push)
157+
last-read-time (:last-read-time stream)
158+
new-status (pump-fn session channel (:id stream) (:stream stream))
159+
now (System/currentTimeMillis)]
160+
(when (and (= pump-fn pull)
161+
(= :eagain new-status)
162+
(< (-> session :options :read-timeout) (- now last-read-time)))
163+
(throw (Exception. (format "Read timeout for %s stream %d"
164+
(-> stream :direction name)
165+
(-> stream :id)))))
166+
(assoc stream :status new-status :last-read-time now))
167+
stream))
168+
169+
(defn pump
170+
"Process a collection of input and output streams all at once."
171+
[session channel input-streams output-streams]
172+
(let [now (System/currentTimeMillis)
173+
write-size (-> session :options :write-chunk-size)
174+
streams (concat (->> input-streams
175+
(map (partial make-stream now :input))
176+
(map (partial ensure-pushback write-size)))
177+
(->> output-streams
178+
(map (partial make-stream now :output))))]
179+
(when-not (empty? streams)
180+
(loop [s (map (partial pump-stream session channel) streams)]
181+
(let [status-set (->> s (map :status) set)]
182+
(if (not= #{:eof} status-set)
183+
(do
184+
(when (contains? status-set :eagain)
185+
(wait session))
186+
(recur (map (partial pump-stream session channel) streams)))
187+
(->> s
188+
(filter #(= :output (:direction %)))
189+
(map #(hash-map (:id %) %))
190+
(apply merge))))))))
191+
192+
(defmacro with-channel
193+
"Convenience macro for wrapping a bunch of channel operations."
194+
[session chan & body]
195+
`(let [~chan (open ~session)]
196+
(try
197+
(do ~@body)
198+
(finally
199+
(close ~session ~chan)
200+
(handle-errors ~session
201+
(free ~chan))))))

src/clj_libssh2/libssh2/keepalive.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
(def config (jna/to-fn Void ssh2/libssh2_keepalive_config))
88

99
; int libssh2_keepalive_send (LIBSSH2_SESSION *session, int *seconds_to_next);
10-
(def send (jna/to-fn Void ssh2/libssh2_keepalive_send))
10+
(def send (jna/to-fn Integer ssh2/libssh2_keepalive_send))

src/clj_libssh2/session.clj

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
; The default options for a session. These are not only the defaults, but an
1212
; exhaustive list of the legal options.
1313
(def default-opts
14-
{:fail-if-not-in-known-hosts false
14+
{:character-set "UTF-8"
15+
:fail-if-not-in-known-hosts false
1516
:fail-unless-known-hosts-matches true
16-
:known-hosts-file nil})
17+
:known-hosts-file nil
18+
:read-chunk-size (* 1024 1024)
19+
:read-timeout 60000
20+
:write-chunk-size (* 1024 1024)})
1721

1822
(defrecord Session [id session socket host port options])
1923

@@ -27,7 +31,7 @@
2731

2832
(defn- create-session-options
2933
[opts]
30-
{:pre [(every? (set (keys default-opts)) (keys opts))]}
34+
{:pre [(map? opts) (every? (set (keys default-opts)) (keys opts))]}
3135
(merge default-opts opts))
3236

3337
(defn- destroy-session
@@ -86,10 +90,11 @@
8690
(socket/connect host port)
8791
host
8892
port
89-
(create-session-options opts))]
93+
session-options)]
9094
(when (> 0 (:socket session))
9195
(destroy-session session "Shutting down due to bad socket." true))
9296
(try
97+
(libssh2-session/set-blocking (:session session) 0)
9398
(handshake session)
9499
(known-hosts/check session)
95100
(authenticate session credentials)
@@ -98,3 +103,18 @@
98103
(catch Exception e
99104
(close session)
100105
(throw e))))))))
106+
107+
(defn get-timeout
108+
[session]
109+
(libssh2-session/get-timeout (:session session)))
110+
111+
(defmacro with-session
112+
[session session-params & body]
113+
`(let [~session (open (:hostname ~session-params)
114+
(:port ~session-params)
115+
(:credentials ~session-params)
116+
(dissoc ~session-params :hostname :port :credentials))]
117+
(try
118+
(do ~@body)
119+
(finally
120+
(close ~session)))))

0 commit comments

Comments
 (0)