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.



48
49
50
51
# File 'lib/osctld/generic/client_handler.rb', line 48

def initialize(socket, opts)
  @sock = socket
  @opts = opts
end

Instance Attribute Details

#optsObject (readonly)

Returns the value of attribute opts.



46
47
48
# File 'lib/osctld/generic/client_handler.rb', line 46

def opts
  @opts
end

Instance Method Details

#communicateObject



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/osctld/generic/client_handler.rb', line 53

def communicate
  v = server_version
  send_data({ version: v }) if v

  loop do
    buf = ''

    while (m = @sock.recv(1024))
      buf += m
      break if m.empty? || m.end_with?("\n")
    end

    break if buf.empty?
    break if parse(buf) == :handled
  end
rescue Errno::ECONNRESET
  # pass
end

#error(msg) ⇒ Object

Signal error ‘msg`



104
105
106
# File 'lib/osctld/generic/client_handler.rb', line 104

def error(msg)
  { status: false, message: msg }
end

#error!(msg) ⇒ Object

Signal error ‘msg`, raises an exception

Raises:



109
110
111
# File 'lib/osctld/generic/client_handler.rb', line 109

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



99
100
101
# File 'lib/osctld/generic/client_handler.rb', line 99

def ok(output = nil)
  { status: true, output: }
end

#parse(data) ⇒ Object (protected)



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/osctld/generic/client_handler.rb', line 131

def parse(data)
  begin
    req = JSON.parse(data, symbolize_names: true)
  rescue TypeError, JSON::ParserError
    return error('syntax error, expected a valid JSON')
  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

#reply_error(err) ⇒ Object



117
118
119
# File 'lib/osctld/generic/client_handler.rb', line 117

def reply_error(err)
  send_data({ status: false, message: err })
end

#reply_ok(res) ⇒ Object



121
122
123
# File 'lib/osctld/generic/client_handler.rb', line 121

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
# File 'lib/osctld/generic/client_handler.rb', line 96

def request_stop; end

#send_data(data) ⇒ Object (protected)



179
180
181
182
183
184
# File 'lib/osctld/generic/client_handler.rb', line 179

def send_data(data)
  @sock.puts(data.to_json)
  true
rescue Errno::EPIPE
  false
end

#send_update(msg) ⇒ Object



113
114
115
# File 'lib/osctld/generic/client_handler.rb', line 113

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



125
126
127
# File 'lib/osctld/generic/client_handler.rb', line 125

def socket
  @sock
end