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
71
# 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 = 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`



107
108
109
# File 'lib/osctld/generic/client_handler.rb', line 107

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

#error!(msg) ⇒ Object

Signal error `msg`, raises an exception

Raises:



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

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)


89
90
91
# File 'lib/osctld/generic/client_handler.rb', line 89

def handle_cmd(req)
  raise NotImplementedError
end

#ok(output = nil) ⇒ Object

Signal command success, send `output` to the client



102
103
104
# File 'lib/osctld/generic/client_handler.rb', line 102

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

#parse(data) ⇒ Object (protected)



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
178
179
180
181
182
183
# File 'lib/osctld/generic/client_handler.rb', line 133

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)

    if !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 => err
    log(:fatal, self, "Error during command execution: #{err.message}")
    log(:fatal, self, denixstorify(err.backtrace).join("\n"))
    reply_error('internal error')
  end

  true
end

#reply_error(err) ⇒ Object



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

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

#reply_ok(res) ⇒ Object



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

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.



97
98
99
# File 'lib/osctld/generic/client_handler.rb', line 97

def request_stop

end

#send_data(data) ⇒ Object (protected)



185
186
187
188
189
190
191
# File 'lib/osctld/generic/client_handler.rb', line 185

def send_data(data)
  @sock.send(data.to_json + "\n", 0)
  true

rescue Errno::EPIPE
  false
end

#send_update(msg) ⇒ Object



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

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)


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

def server_version
  nil
end

#socketObject



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

def socket
  @sock
end