Class: OsCtld::Generic::ClientHandler
- Inherits:
-
Object
- Object
- OsCtld::Generic::ClientHandler
- Includes:
- OsCtl::Lib::Utils::Exception, OsCtl::Lib::Utils::Log
- Defined in:
- lib/osctld/generic/client_handler.rb
Overview
Generic client handler for Server
This class cannot be used directly, you need to subclass it and implement template methods that will provide your logic. This class implements only the generic communication protocol.
Protocol
The protocol is line-based, each line containing a JSON formatted message. Client with the server can negoatiate a different protocol and hijack the socket, e.g. this is used when the client is attaching a console or executing command within a container.
Upon connection, the server sends the client its version, if the server implementation provides it:
{version: "version"}
The client may decide to close the connection when an unsupported version is detected.
When the version is accepted, the client sends a command:
{cmd: <command>, opts: <command parameters>}
The client waits for the server to reply. While waiting for the final response, the client can receive a progress update:
{status: true, progress: "message"}
There can be zero or multiple progress updates, followed by a final response:
{status: true, response: <command response>}
{status: false, message: "error message"}
Based on the response, the client either exits, sends another command, or the connection can be hijacked and another communication protocol may be used.
Direct Known Subclasses
Daemon::ClientHandler, SendReceive::Server::ClientHandler, UserControl::Supervisor::ClientHandler, UserControl::Supervisor::NamespacedClientHandler
Instance Attribute Summary collapse
-
#opts ⇒ Object
readonly
Returns the value of attribute opts.
Instance Method Summary collapse
- #close_socket ⇒ Object protected
- #communicate ⇒ Object
-
#error(msg) ⇒ Object
Signal error
msg. -
#error!(msg) ⇒ Object
Signal error
msg, raises an exception. -
#handle_cmd(req) ⇒ {status: true, output: any}, ...
Handle client command and return a response or an error.
-
#initialize(socket, opts) ⇒ ClientHandler
constructor
A new instance of ClientHandler.
-
#ok(output = nil) ⇒ Object
Signal command success, send
outputto the client. - #parse(data) ⇒ Object protected
- #read_request ⇒ Object protected
- #reply_error(err) ⇒ Object
- #reply_ok(res) ⇒ Object
-
#request_stop ⇒ Object
Stop the client thread if possible.
- #send_data(data) ⇒ Object protected
- #send_update(msg) ⇒ Object
-
#server_version ⇒ String
Return the server version that is sent to the client in the first message.
- #socket ⇒ Object
- #stop_requested? ⇒ Boolean protected
Constructor Details
#initialize(socket, opts) ⇒ ClientHandler
Returns a new instance of ClientHandler.
49 50 51 52 53 |
# File 'lib/osctld/generic/client_handler.rb', line 49 def initialize(socket, opts) @sock = socket @opts = opts @stop_requested = false end |
Instance Attribute Details
#opts ⇒ Object (readonly)
Returns the value of attribute opts.
47 48 49 |
# File 'lib/osctld/generic/client_handler.rb', line 47 def opts @opts end |
Instance Method Details
#close_socket ⇒ Object (protected)
223 224 225 226 227 |
# File 'lib/osctld/generic/client_handler.rb', line 223 def close_socket @sock.close unless @sock.closed? rescue IOError # pass end |
#communicate ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/osctld/generic/client_handler.rb', line 55 def communicate hijacked = false v = server_version send_data({ version: v }) if v loop do buf = read_request break if buf.nil? || buf.empty? if parse(buf) == :handled hijacked = true break end break if stop_requested? end rescue Errno::ECONNRESET, IOError # pass ensure # Normal client EOF must not leave the accepted socket waiting for GC. # A hijacked connection belongs to its new protocol handler instead. close_socket unless hijacked end |
#error(msg) ⇒ Object
Signal error msg
114 115 116 |
# File 'lib/osctld/generic/client_handler.rb', line 114 def error(msg) { status: false, message: msg } end |
#error!(msg) ⇒ Object
Signal error msg, raises an exception
119 120 121 |
# File 'lib/osctld/generic/client_handler.rb', line 119 def error!(msg) raise CommandFailed, msg end |
#handle_cmd(req) ⇒ {status: true, output: any}, ...
96 97 98 |
# File 'lib/osctld/generic/client_handler.rb', line 96 def handle_cmd(req) raise NotImplementedError end |
#ok(output = nil) ⇒ Object
Signal command success, send output to the client
109 110 111 |
# File 'lib/osctld/generic/client_handler.rb', line 109 def ok(output = nil) { status: true, output: } end |
#parse(data) ⇒ Object (protected)
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/osctld/generic/client_handler.rb', line 162 def parse(data) begin req = JSON.parse(data, symbolize_names: true) rescue TypeError, JSON::ParserError reply_error('syntax error, expected a valid JSON') return true end unless req.is_a?(Hash) reply_error('invalid input') return true end log(:debug, self, "Received command '#{req[:cmd]}'") begin ret = handle_cmd(req) unless ret.is_a?(Hash) log(:fatal, self, "Unrecognized return value #{ret.class}, expected Hash") reply_error('internal error') return end if ret[:status] === true reply_ok(ret[:output]) elsif ret[:status] === :handled log(:debug, self, 'Connection hijacked') return :handled elsif !ret[:message] log(:fatal, self, 'Command failed, but no error message provided') reply_error('internal error') else reply_error(ret[:message]) end rescue CommandFailed, ResourceLocked => e reply_error(e.) rescue DeadlockDetected => e log(:fatal, self, 'Possible deadlock detected') log(:fatal, self, denixstorify(e.backtrace).join("\n")) LockRegistry.dump reply_error('internal error') rescue StandardError => e log(:fatal, self, "Error during command execution: #{e.}") log(:fatal, self, denixstorify(e.backtrace).join("\n")) reply_error('internal error') end true end |
#read_request ⇒ Object (protected)
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/osctld/generic/client_handler.rb', line 145 def read_request buf = +'' loop do return if stop_requested? readable = @sock.wait_readable(0.2) next unless readable m = @sock.recv(1024) return buf if m.nil? || m.empty? buf += m return buf if m.end_with?("\n") end end |
#reply_error(err) ⇒ Object
127 128 129 |
# File 'lib/osctld/generic/client_handler.rb', line 127 def reply_error(err) send_data({ status: false, message: err }) end |
#reply_ok(res) ⇒ Object
131 132 133 |
# File 'lib/osctld/generic/client_handler.rb', line 131 def reply_ok(res) send_data({ status: true, response: res }) end |
#request_stop ⇒ Object
Stop the client thread if possible
This method is not called from the client thread, so the implementation has to communicate with the thread and tell it to quit.
104 105 106 |
# File 'lib/osctld/generic/client_handler.rb', line 104 def request_stop @stop_requested = true end |
#send_data(data) ⇒ Object (protected)
216 217 218 219 220 221 |
# File 'lib/osctld/generic/client_handler.rb', line 216 def send_data(data) @sock.puts(data.to_json) true rescue Errno::EPIPE false end |
#send_update(msg) ⇒ Object
123 124 125 |
# File 'lib/osctld/generic/client_handler.rb', line 123 def send_update(msg) send_data({ status: true, progress: msg }) end |
#server_version ⇒ String
Return the server version that is sent to the client in the first message. By default, no version is sent to the client.
83 84 85 |
# File 'lib/osctld/generic/client_handler.rb', line 83 def server_version nil end |
#socket ⇒ Object
135 136 137 |
# File 'lib/osctld/generic/client_handler.rb', line 135 def socket @sock end |
#stop_requested? ⇒ Boolean (protected)
141 142 143 |
# File 'lib/osctld/generic/client_handler.rb', line 141 def stop_requested? @stop_requested end |