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

#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

Raises:



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

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)


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.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_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.

Returns:

  • (String)


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)

Returns:

  • (Boolean)


141
142
143
# File 'lib/osctld/generic/client_handler.rb', line 141

def stop_requested?
  @stop_requested
end