Class: OsCtld::Generic::ClientHandler

Inherits:
Object
  • Object
show all
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.

Instance Attribute Summary collapse

Instance Method Summary collapse

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

#optsObject (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_socketObject (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

#communicateObject



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

Raises:



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}, ...

Handle client command and return a response or an error.

Use return #ok, #error, #error! to return response, or report error.

Parameters:

  • req (Hash)

    client request

Returns:

  • ({status: true, output: any})
  • ({status: false, message: String})
  • ({status: :handled})

Raises:

  • (NotImplementedError)


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.message)
  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.message}")
    log(:fatal, self, denixstorify(e.backtrace).join("\n"))
    reply_error('internal error')
  end

  true
end

#read_requestObject (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_stopObject

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_versionString

Return the server version that is sent to the client in the first message. By default, no version is sent to the client.

Returns:

  • (String)


75
76
77
# File 'lib/osctld/generic/client_handler.rb', line 75

def server_version
  nil
end

#socketObject



127
128
129
# File 'lib/osctld/generic/client_handler.rb', line 127

def socket
  @sock
end

#stop_requested?Boolean (protected)

Returns:

  • (Boolean)


133
134
135
# File 'lib/osctld/generic/client_handler.rb', line 133

def stop_requested?
  @stop_requested
end