Add --debug flag for WebSocket connection diagnostics

Shows WS URL, auth headers (truncated), full handshake request/response.
Also improves the handshake failure error to include the raw server response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 13:09:01 +01:00
parent c59deca51e
commit 9fe6ef339a
4 changed files with 26 additions and 8 deletions
+3 -2
View File
@@ -39,6 +39,7 @@ module PortainerCli
HELP HELP
def self.run(argv) def self.run(argv)
debug = argv.delete('--debug')
config = Config.load config = Config.load
cmd = argv.shift&.downcase cmd = argv.shift&.downcase
@@ -56,11 +57,11 @@ module PortainerCli
when 'console' when 'console'
require_config!(config) require_config!(config)
client = Client.new(config) client = Client.new(config)
Commands::Console.new(config, client).run(argv) Commands::Console.new(config, client, debug: debug).run(argv)
when 'exec', 'shell' when 'exec', 'shell'
require_config!(config) require_config!(config)
client = Client.new(config) client = Client.new(config)
Commands::Exec.new(config, client).run(argv) Commands::Exec.new(config, client, debug: debug).run(argv)
when 'version', '--version', '-v' when 'version', '--version', '-v'
puts "portainer-cli #{PortainerCli::VERSION}" puts "portainer-cli #{PortainerCli::VERSION}"
when 'help', '--help', '-h', nil when 'help', '--help', '-h', nil
+3 -2
View File
@@ -13,9 +13,10 @@ module PortainerCli
class Console class Console
include Base include Base
def initialize(config, client) def initialize(config, client, debug: false)
@config = config @config = config
@client = client @client = client
@debug = debug
end end
def run(args) def run(args)
@@ -42,7 +43,7 @@ module PortainerCli
send_resize(endpoint_id, exec_id) if $stdout.tty? send_resize(endpoint_id, exec_id) if $stdout.tty?
trap('SIGWINCH') { send_resize(endpoint_id, exec_id) } if $stdout.tty? trap('SIGWINCH') { send_resize(endpoint_id, exec_id) } if $stdout.tty?
WebsocketExec.new(ws_url, @config.auth_header, ssl_verify: @config.ssl_verify).run WebsocketExec.new(ws_url, @config.auth_header, ssl_verify: @config.ssl_verify, debug: @debug).run
$stderr.puts dim("\r\nSession ended.") $stderr.puts dim("\r\nSession ended.")
rescue PortainerCli::Client::ApiError => e rescue PortainerCli::Client::ApiError => e
+3 -2
View File
@@ -9,9 +9,10 @@ module PortainerCli
class Exec class Exec
include Base include Base
def initialize(config, client) def initialize(config, client, debug: false)
@config = config @config = config
@client = client @client = client
@debug = debug
end end
# portainer-cli exec <endpoint-id> <container-name-or-id> [-- cmd args...] # portainer-cli exec <endpoint-id> <container-name-or-id> [-- cmd args...]
@@ -36,7 +37,7 @@ module PortainerCli
send_resize(endpoint_id, exec_id) if $stdout.tty? send_resize(endpoint_id, exec_id) if $stdout.tty?
trap('SIGWINCH') { send_resize(endpoint_id, exec_id) } if $stdout.tty? trap('SIGWINCH') { send_resize(endpoint_id, exec_id) } if $stdout.tty?
WebsocketExec.new(ws_url, @config.auth_header, ssl_verify: @config.ssl_verify).run WebsocketExec.new(ws_url, @config.auth_header, ssl_verify: @config.ssl_verify, debug: @debug).run
$stderr.puts dim("\r\nSession ended.") $stderr.puts dim("\r\nSession ended.")
rescue PortainerCli::Client::ApiError => e rescue PortainerCli::Client::ApiError => e
+17 -2
View File
@@ -10,17 +10,23 @@ module PortainerCli
class WebsocketExec class WebsocketExec
READ_SIZE = 4096 READ_SIZE = 4096
def initialize(url, auth_header, ssl_verify: true) def initialize(url, auth_header, ssl_verify: true, debug: false)
@url = URI.parse(url) @url = URI.parse(url)
@auth_header = auth_header @auth_header = auth_header
@ssl_verify = ssl_verify @ssl_verify = ssl_verify
@debug = debug
end end
def run def run
debug "WS URL: #{@url}"
debug "Auth headers: #{@auth_header.map { |k, v| "#{k}: #{v[0, 8]}..." }.join(', ')}"
socket = open_socket socket = open_socket
handshake = perform_handshake(socket) handshake = perform_handshake(socket)
raise "WebSocket handshake failed: #{handshake.error}" unless handshake.valid? unless handshake.valid?
raise "WebSocket handshake failed: #{handshake.error}\nServer response:\n#{handshake.instance_variable_get(:@data)}"
end
run_io_loop(socket) run_io_loop(socket)
ensure ensure
@@ -50,18 +56,27 @@ module PortainerCli
url: @url.to_s, url: @url.to_s,
headers: @auth_header headers: @auth_header
) )
debug "Handshake request:\n#{handshake.to_s.chomp}"
socket.write(handshake.to_s) socket.write(handshake.to_s)
response = +''
loop do loop do
byte = socket.read(1) byte = socket.read(1)
break if byte.nil? break if byte.nil?
response << byte
handshake << byte handshake << byte
break if handshake.finished? break if handshake.finished?
end end
debug "Handshake response:\n#{response.chomp}"
handshake handshake
end end
def debug(msg)
return unless @debug
$stderr.puts "\e[2m[debug] #{msg}\e[0m"
end
def run_io_loop(socket) def run_io_loop(socket)
incoming = WebSocket::Frame::Incoming::Client.new incoming = WebSocket::Frame::Incoming::Client.new