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 ‘output` to 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)
215 216 217 218 219 |
# File 'lib/osctld/generic/client_handler.rb', line 215 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 |
# File 'lib/osctld/generic/client_handler.rb', line 55 def communicate v = server_version send_data({ version: v }) if v loop do buf = read_request break if buf.nil? || buf.empty? break if parse(buf) == :handled break if stop_requested? end rescue Errno::ECONNRESET, IOError # pass ensure close_socket if stop_requested? end |
#error(msg) ⇒ Object
Signal error ‘msg`
106 107 108 |
# File 'lib/osctld/generic/client_handler.rb', line 106 def error(msg) { status: false, message: msg } end |
#error!(msg) ⇒ Object
Signal error ‘msg`, raises an exception
111 112 113 |
# File 'lib/osctld/generic/client_handler.rb', line 111 def error!(msg) raise CommandFailed, msg end |
#handle_cmd(req) ⇒ {status: true, output: any}, ...
88 89 90 |
# File 'lib/osctld/generic/client_handler.rb', line 88 def handle_cmd(req) raise NotImplementedError end |
#ok(output = nil) ⇒ Object
Signal command success, send ‘output` to the client
101 102 103 |
# File 'lib/osctld/generic/client_handler.rb', line 101 def ok(output = nil) { status: true, output: } end |
#parse(data) ⇒ Object (protected)
154 155 156 157 158 159 160 161 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 |
# File 'lib/osctld/generic/client_handler.rb', line 154 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)
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/osctld/generic/client_handler.rb', line 137 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
119 120 121 |
# File 'lib/osctld/generic/client_handler.rb', line 119 def reply_error(err) send_data({ status: false, message: err }) end |
#reply_ok(res) ⇒ Object
123 124 125 |
# File 'lib/osctld/generic/client_handler.rb', line 123 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.
96 97 98 |
# File 'lib/osctld/generic/client_handler.rb', line 96 def request_stop @stop_requested = true end |
#send_data(data) ⇒ Object (protected)
208 209 210 211 212 213 |
# File 'lib/osctld/generic/client_handler.rb', line 208 def send_data(data) @sock.puts(data.to_json) true rescue Errno::EPIPE false end |
#send_update(msg) ⇒ Object
115 116 117 |
# File 'lib/osctld/generic/client_handler.rb', line 115 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.
75 76 77 |
# File 'lib/osctld/generic/client_handler.rb', line 75 def server_version nil end |
#socket ⇒ Object
127 128 129 |
# File 'lib/osctld/generic/client_handler.rb', line 127 def socket @sock end |
#stop_requested? ⇒ Boolean (protected)
133 134 135 |
# File 'lib/osctld/generic/client_handler.rb', line 133 def stop_requested? @stop_requested end |