Initial Meow MUD client

This commit is contained in:
2026-09-17 01:08:16 +02:00
commit 5e6d9138c4
9 changed files with 1123 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
require "securerandom"
module MeowClient
ConnectionProfile = Struct.new(
:id, :name, :host, :port, :tls, :encoding, :auto_read,
keyword_init: true
) do
def to_h
{
"id" => id,
"name" => name,
"host" => host,
"port" => port,
"tls" => tls == true,
"encoding" => encoding,
"auto_read" => auto_read != false
}
end
end
class ProfileRepository
FILE_NAME = "profiles.json"
SCHEMA_VERSION = 1
ENCODINGS = {
"UTF-8" => "UTF-8",
"Windows-1252" => "Windows-1252",
"ISO-8859-1" => "ISO-8859-1"
}.freeze
def initialize(program)
@program = program
end
def all
data = @program.read_json(FILE_NAME, :default => default_document)
return [] unless data.is_a?(Hash)
rows = data["profiles"]
return [] unless rows.is_a?(Array)
rows.filter_map { |row| profile_from(row) }
rescue Exception => error
Log.warning("Meow profile read failed: #{error.class}: #{error.message}") if defined?(Log)
[]
end
def save(profile)
normalized = normalize(profile)
@program.update_json(FILE_NAME, :default => default_document) do |data|
data.replace(default_document) unless data.is_a?(Hash)
data["version"] = SCHEMA_VERSION
data["profiles"] = [] unless data["profiles"].is_a?(Array)
index = data["profiles"].index { |row| row.is_a?(Hash) && row["id"].to_s == normalized.id }
if index == nil
data["profiles"] << normalized.to_h
else
data["profiles"][index] = normalized.to_h
end
end
normalized
end
def delete(id)
@program.update_json(FILE_NAME, :default => default_document) do |data|
data.replace(default_document) unless data.is_a?(Hash)
data["profiles"] = [] unless data["profiles"].is_a?(Array)
data["profiles"].delete_if { |row| row.is_a?(Hash) && row["id"].to_s == id.to_s }
end
end
def build(attributes = {})
ConnectionProfile.new(
:id => attributes[:id] || SecureRandom.uuid,
:name => attributes[:name] || "",
:host => attributes[:host] || "",
:port => attributes[:port] || 23,
:tls => attributes[:tls] == true,
:encoding => attributes[:encoding] || "UTF-8",
:auto_read => attributes.fetch(:auto_read, true)
)
end
def validate(profile)
errors = []
errors << _("Enter a profile name.") if profile.name.to_s.strip == ""
errors << _("Enter a server host name.") if profile.host.to_s.strip == ""
port = Integer(profile.port, :exception => false)
errors << _("Port must be between 1 and 65535.") if port == nil || port < 1 || port > 65_535
errors << _("Choose a supported character encoding.") unless ENCODINGS.value?(profile.encoding.to_s)
errors
end
private
def default_document
{"version" => SCHEMA_VERSION, "profiles" => []}
end
def normalize(profile)
ConnectionProfile.new(
:id => profile.id.to_s == "" ? SecureRandom.uuid : profile.id.to_s,
:name => profile.name.to_s.strip,
:host => profile.host.to_s.strip,
:port => Integer(profile.port, :exception => false) || 23,
:tls => profile.tls == true,
:encoding => ENCODINGS.value?(profile.encoding.to_s) ? profile.encoding.to_s : "UTF-8",
:auto_read => profile.auto_read != false
)
end
def profile_from(row)
return nil unless row.is_a?(Hash)
profile = build(
:id => row["id"],
:name => row["name"],
:host => row["host"],
:port => row["port"],
:tls => row["tls"] == true,
:encoding => row["encoding"],
:auto_read => row["auto_read"] != false
)
validate(profile).empty? ? profile : nil
end
end
end
+262
View File
@@ -0,0 +1,262 @@
module MeowClient
class Session
MAX_LINES = 5_000
MAX_BYTES = 1_048_576
MAX_HISTORY = 100
MAX_SPEECH = 200
SPEECH_CHUNK = 600
PASSWORD_PATTERN = /\b(?:password|passphrase|secret|token|pin)\b.*[:?>]\s*\z/i
def initialize(program, profile, transport)
@program = program
@profile = profile
@transport = transport
@pipeline = TextPipeline.new(profile.encoding)
@lines = []
@line_bytes = 0
@history = []
@history_index = nil
@speech_queue = []
@speech_paused = profile.auto_read == false
@speech_overflow = 0
@sensitive_next = false
@closed = false
build_form
end
def run
@form.wait
ensure
@transport.close
@program.release(@transport, :close => false)
end
private
def build_form
@status = Static.new(_("Connected to %{name}.") % {:name => @profile.name})
@output = EditBox.new(
_("MUD output"),
:type => EditBox::Flags::ReadOnly | EditBox::Flags::MultiLine,
:text => "",
:detect_text_links => false
)
@command = EditBox.new(_("Command"), :text => "")
@send = Button.new(_("Send"))
@speech_button = Button.new(@speech_paused ? _("Resume automatic reading") : _("Pause automatic reading"))
@stop_speech = Button.new(_("Stop current speech"))
@disconnect = Button.new(_("Disconnect"))
@form = Form.new(
[@status, @output, @command, @send, @speech_button, @stop_speech, @disconnect],
:quiet => true
)
@form.accept_button = @send
@form.cancel_button = @disconnect
@form.add_timer(FormTimer.new(0.05, :repeat => true) { tick })
@send.on(:press) { send_command }
@speech_button.on(:press) { toggle_speech }
@stop_speech.on(:press) { speech_stop }
@disconnect.on(:press) { request_disconnect }
@output.add_tip(_("Use the context menu to jump to the latest output."))
@output.bind_context do |menu|
menu.option(_("Jump to latest output")) { jump_to_latest }
end
@command.add_tip(_("Use the context menu to recall commands or mark the next command as sensitive."))
@command.bind_context do |menu|
menu.option(_("Previous command")) { recall(-1) }
menu.option(_("Next command")) { recall(1) }
menu.option(_("Sensitive command")) { set_sensitive(true) }
menu.option(_("Resume reading from latest output")) { resume_from_latest }
end
add_system_line(_("Connected to %{host} on port %{port}.") % {
:host => @profile.host,
:port => @profile.port
})
end
def tick
drain_events
if @pipeline.prompt_due?
record = @pipeline.flush_prompt
receive_record(record) if record != nil
end
dispatch_speech
rescue Exception => error
Log.error("Meow session update failed: #{error.class}: #{error.message}") if defined?(Log)
add_system_line(_("Session update error: %{message}") % {:message => error.message})
close_form
end
def drain_events
loop do
event = @transport.events.pop(true)
case event.type
when :bytes
result = @pipeline.feed(event.payload)
result.replies.each { |reply| @transport.send_bytes(reply) }
result.records.each { |record| receive_record(record) }
when :disconnected, :error
@pipeline.finish.each { |record| receive_record(record) }
add_system_line(event.payload.to_s)
close_form
end
end
rescue ThreadError
end
def receive_record(record)
text = record.to_s
append_line(text)
@sensitive_next = true if text.strip.match?(PASSWORD_PATTERN)
apply_sensitive_flag
enqueue_speech(text)
end
def append_line(text)
value = text.to_s
@lines << value
@line_bytes += value.bytesize + 1
while @lines.size > MAX_LINES || @line_bytes > MAX_BYTES
removed = @lines.shift.to_s
@line_bytes -= removed.bytesize + 1
end
@output.set_text(@lines.join("\n"), false)
end
def add_system_line(text)
append_line("[#{text}]")
end
def send_command
text = @command.text.to_s
return if text == ""
begin
encoded = text.encode(@profile.encoding)
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
alert(_("This command contains characters that cannot be represented by %{encoding}.") % {
:encoding => @profile.encoding
})
return
end
unless @transport.send_bytes(encoded.b + "\r\n".b)
alert(_("The connection is no longer available."))
close_form
return
end
sensitive = @sensitive_next
unless sensitive
add_history(text)
append_line("> #{text}") unless @pipeline.remote_echo?
end
@command.set_text("")
@history_index = nil
set_sensitive(false, false)
end
def add_history(text)
@history.delete(text)
@history << text
@history.shift while @history.size > MAX_HISTORY
end
def recall(direction)
return alert(_("No command history is available."), false) if @history.empty?
if @history_index == nil
@history_index = direction < 0 ? @history.size - 1 : 0
else
@history_index = [[@history_index + direction, 0].max, @history.size - 1].min
end
@command.set_text(@history[@history_index])
@command.index = @command.text.length
@command.check = @command.index
set_sensitive(false, false)
@form.index = @command
@command.focus
end
def set_sensitive(value, announce = true)
@sensitive_next = value == true
apply_sensitive_flag
if announce
speak(@sensitive_next ? _("Sensitive input enabled.") : _("Sensitive input disabled."))
end
end
def apply_sensitive_flag
if @sensitive_next
@command.flags |= EditBox::Flags::Password
else
@command.flags &= ~EditBox::Flags::Password
end
end
def enqueue_speech(text)
return if text.strip == ""
chunks = split_speech(text)
chunks.each do |chunk|
if @speech_queue.size >= MAX_SPEECH
@speech_queue.shift
@speech_overflow += 1
end
@speech_queue << chunk
end
end
def split_speech(text)
value = text.to_s.strip
return [] if value == ""
value.scan(/.{1,#{SPEECH_CHUNK}}/m).map(&:strip).reject(&:empty?)
end
def dispatch_speech
return if @speech_paused || speech_actived
if @speech_overflow > 0
count = @speech_overflow
@speech_overflow = 0
speak(n_("%{count} older line was skipped.", "%{count} older lines were skipped.", count) % {:count => count})
return
end
text = @speech_queue.shift
speak(text, :stop => false) if text != nil
end
def toggle_speech
@speech_paused = !@speech_paused
@speech_button.label = @speech_paused ? _("Resume automatic reading") : _("Pause automatic reading")
speak(@speech_paused ? _("Automatic reading paused.") : _("Automatic reading resumed."))
end
def resume_from_latest
@speech_queue.clear
@speech_overflow = 0
@speech_paused = false
@speech_button.label = _("Pause automatic reading")
speak(_("Automatic reading resumed from the latest output."))
end
def jump_to_latest
position = @output.text.length
@output.index = position
@output.check = position
@form.index = @output
@output.focus
end
def request_disconnect
if @closed || confirm(_("Disconnect from %{name}?") % {:name => @profile.name})
add_system_line(_("Disconnected."))
close_form
end
end
def close_form
return if @closed
@closed = true
@transport.close
@form.resume
end
end
end
+148
View File
@@ -0,0 +1,148 @@
module MeowClient
module Telnet
IAC = 255
DONT = 254
DO = 253
WONT = 252
WILL = 251
SB = 250
SE = 240
ECHO = 1
SUPPRESS_GO_AHEAD = 3
TERMINAL_TYPE = 24
TTYPE_IS = 0
TTYPE_SEND = 1
Result = Struct.new(:data, :replies, :events, keyword_init: true)
class Parser
attr_reader :remote_echo
def initialize
@state = :data
@negotiation_command = nil
@subnegotiation_option = nil
@subnegotiation = +"".b
@remote_echo = false
@remote_options = {}
@local_options = {}
end
def feed(bytes)
output = +"".b
replies = []
events = []
bytes.to_s.b.each_byte do |byte|
case @state
when :data
if byte == IAC
@state = :iac
else
output << byte
end
when :iac
handle_iac(byte, output)
when :negotiation
reply, event = negotiate(@negotiation_command, byte)
replies << reply if reply != nil
events << event if event != nil
@state = :data
when :sb_option
@subnegotiation_option = byte
@subnegotiation.clear
@state = :sb
when :sb
if byte == IAC
@state = :sb_iac
else
@subnegotiation << byte
end
when :sb_iac
if byte == IAC
@subnegotiation << IAC
@state = :sb
elsif byte == SE
reply, event = finish_subnegotiation
replies << reply if reply != nil
events << event if event != nil
@state = :data
else
@state = :data
end
end
end
Result.new(:data => output, :replies => replies, :events => events)
end
private
def handle_iac(byte, output)
case byte
when IAC
output << IAC
@state = :data
when WILL, WONT, DO, DONT
@negotiation_command = byte
@state = :negotiation
when SB
@state = :sb_option
else
@state = :data
end
end
def packet(command, option)
[IAC, command, option].pack("C*")
end
def negotiate(command, option)
case command
when WILL
enabled = [ECHO, SUPPRESS_GO_AHEAD].include?(option)
changed = @remote_options[option] != enabled
@remote_options[option] = enabled
return [nil, nil] unless changed
if option == ECHO
@remote_echo = enabled
return [packet(enabled ? DO : DONT, option), [:remote_echo, enabled]]
end
[packet(enabled ? DO : DONT, option), nil]
when WONT
changed = @remote_options[option] != false
@remote_options[option] = false
if option == ECHO
@remote_echo = false
return [packet(DONT, option), [:remote_echo, false]] if changed
end
[changed ? packet(DONT, option) : nil, nil]
when DO
enabled = [SUPPRESS_GO_AHEAD, TERMINAL_TYPE].include?(option)
changed = @local_options[option] != enabled
@local_options[option] = enabled
[changed ? packet(enabled ? WILL : WONT, option) : nil, nil]
when DONT
changed = @local_options[option] != false
@local_options[option] = false
[changed ? packet(WONT, option) : nil, nil]
else
[nil, nil]
end
end
def finish_subnegotiation
if @subnegotiation_option == TERMINAL_TYPE &&
@local_options[TERMINAL_TYPE] == true &&
@subnegotiation.getbyte(0) == TTYPE_SEND
body = [IAC, SB, TERMINAL_TYPE, TTYPE_IS].pack("C*") + "MEOW".b
return [body + [IAC, SE].pack("C*"), [:terminal_type, "MEOW"]]
end
[nil, [:subnegotiation, @subnegotiation_option]]
ensure
@subnegotiation_option = nil
@subnegotiation.clear
end
end
end
end
+180
View File
@@ -0,0 +1,180 @@
module MeowClient
class AnsiFilter
def initialize
@state = :text
end
def feed(bytes)
output = +"".b
bytes.to_s.b.each_byte do |byte|
case @state
when :text
if byte == 27
@state = :escape
elsif byte == 9 || byte == 10 || byte == 13 || byte >= 32
output << byte
end
when :escape
if byte == 91
@state = :csi
elsif byte == 93
@state = :osc
else
@state = :text
end
when :csi
@state = :text if byte >= 64 && byte <= 126
when :osc
if byte == 7
@state = :text
elsif byte == 27
@state = :osc_escape
end
when :osc_escape
@state = byte == 92 ? :text : :osc
end
end
output
end
end
class TextPipeline
Result = Struct.new(:records, :replies, :events, keyword_init: true)
def initialize(encoding)
@telnet = Telnet::Parser.new
@ansi = AnsiFilter.new
@encoding = encoding.to_s
@decode_pending = +"".b
@line = +""
@pending_cr = false
@last_text_at = nil
end
def remote_echo?
@telnet.remote_echo == true
end
def feed(bytes)
parsed = @telnet.feed(bytes)
filtered = @ansi.feed(parsed.data)
text = decode(filtered)
records = frame(text)
@last_text_at = monotonic_time if text != ""
Result.new(:records => records, :replies => parsed.replies, :events => parsed.events)
end
def prompt_due?(delay = 0.2)
@line != "" && @last_text_at != nil && monotonic_time - @last_text_at >= delay
end
def flush_prompt
return nil if @line == ""
value = @line.dup
@line.clear
@pending_cr = false
@last_text_at = nil
value
end
def finish
tail = if @encoding == "UTF-8"
value = @decode_pending.dup.force_encoding(Encoding::UTF_8).scrub
@decode_pending.clear
value
else
""
end
records = frame(tail)
prompt = flush_prompt
records << prompt if prompt != nil
records
rescue Exception
prompt = flush_prompt
prompt == nil ? [] : [prompt]
end
private
def decode(bytes)
if @encoding != "UTF-8"
return bytes.to_s.b.force_encoding(@encoding).encode(
Encoding::UTF_8,
:invalid => :replace,
:undef => :replace,
:replace => "\uFFFD"
)
end
@decode_pending << bytes.to_s.b
keep = incomplete_utf8_suffix_length(@decode_pending)
ready_size = @decode_pending.bytesize - keep
ready = @decode_pending.byteslice(0, ready_size).to_s.b
@decode_pending = keep == 0 ? +"".b : @decode_pending.byteslice(ready_size, keep).to_s.b
ready.force_encoding(Encoding::UTF_8).scrub
end
def incomplete_utf8_suffix_length(bytes)
size = bytes.bytesize
return 0 if size == 0
continuation = 0
index = size - 1
while index >= 0 && continuation < 3 && (bytes.getbyte(index) & 0xC0) == 0x80
continuation += 1
index -= 1
end
return 0 if index < 0
lead = bytes.getbyte(index)
expected = if (lead & 0x80) == 0
1
elsif (lead & 0xE0) == 0xC0
2
elsif (lead & 0xF0) == 0xE0
3
elsif (lead & 0xF8) == 0xF0
4
else
1
end
available = size - index
available < expected ? available : 0
end
def frame(text)
records = []
text.each_char do |character|
if @pending_cr
if character == "\n" || character == "\0"
records << take_line
@pending_cr = false
next
else
records << take_line
@pending_cr = false
end
end
case character
when "\r"
@pending_cr = true
when "\n"
records << take_line
when "\0"
else
@line << character
end
end
records
end
def take_line
value = @line.dup
@line.clear
value
end
def monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end
end
+139
View File
@@ -0,0 +1,139 @@
require "socket"
require "openssl"
module MeowClient
TransportEvent = Struct.new(:type, :payload, keyword_init: true)
class Transport
CONNECT_TIMEOUT = 15
attr_reader :events
def self.connect(profile)
tcp = nil
socket = nil
begin
tcp = Socket.tcp(profile.host, profile.port, :connect_timeout => CONNECT_TIMEOUT)
if profile.tls
context = EltenAPI::TLS.client_context
socket = OpenSSL::SSL::SSLSocket.new(tcp, context)
socket.hostname = profile.host if socket.respond_to?(:hostname=)
socket.sync_close = true
socket.connect
else
socket = tcp
end
new(socket)
rescue Exception
begin
socket.close if socket != nil
rescue Exception
end
begin
tcp.close if tcp != nil && tcp != socket
rescue Exception
end
raise
end
end
def initialize(socket)
@socket = socket
@events = Queue.new
@outbound = Queue.new
@write_buffer = +"".b
@close_mutex = Mutex.new
@closed = false
@thread = Thread.new { run }
@thread.report_on_exception = false
end
def send_bytes(bytes)
return false if closed?
@outbound << bytes.to_s.b
true
end
def close
thread = nil
@close_mutex.synchronize do
return if @closed
@closed = true
thread = @thread
begin
@socket.close
rescue Exception
end
end
thread.join(1.0) if thread != nil && thread != Thread.current
nil
end
def closed?
@close_mutex.synchronize { @closed }
end
private
def run
@events << TransportEvent.new(:type => :connected)
loop do
break if closed?
fill_write_buffer
readable = [@socket]
writable = @write_buffer.empty? ? [] : [@socket]
selected = IO.select(readable, writable, nil, 0.1)
next if selected == nil
read_available if selected[0].include?(@socket)
write_available if selected[1].include?(@socket)
end
rescue EOFError
@events << TransportEvent.new(:type => :disconnected, :payload => _("The server closed the connection."))
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError => error
unless closed?
@events << TransportEvent.new(
:type => :error,
:payload => _("Connection error: %{message}") % {:message => error.message}
)
end
rescue Exception => error
unless closed?
Log.error("Meow transport failure: #{error.class}: #{error.message}") if defined?(Log)
@events << TransportEvent.new(
:type => :error,
:payload => _("Unexpected connection error: %{message}") % {:message => error.message}
)
end
ensure
@close_mutex.synchronize { @closed = true }
begin
@socket.close
rescue Exception
end
end
def fill_write_buffer
loop do
@write_buffer << @outbound.pop(true)
end
rescue ThreadError
end
def read_available
data = @socket.read_nonblock(16_384)
raise EOFError if data == nil || data == ""
@events << TransportEvent.new(:type => :bytes, :payload => data.b)
rescue IO::WaitReadable, OpenSSL::SSL::SSLErrorWaitReadable
rescue IO::WaitWritable, OpenSSL::SSL::SSLErrorWaitWritable
end
def write_available
return if @write_buffer.empty?
count = @socket.write_nonblock(@write_buffer)
@write_buffer = @write_buffer.byteslice(count..-1).to_s.b
rescue IO::WaitWritable, OpenSSL::SSL::SSLErrorWaitWritable
rescue IO::WaitReadable, OpenSSL::SSL::SSLErrorWaitReadable
end
end
end
+170
View File
@@ -0,0 +1,170 @@
module MeowClient
class UI
def initialize(program)
@program = program
@profiles = ProfileRepository.new(program)
end
def main
loop do
action, profile = connection_manager
case action
when :connect
connect(profile)
when :new
edit_profile(nil)
when :edit
edit_profile(profile)
when :delete
delete_profile(profile)
else
break
end
end
end
private
def connection_manager
profiles = @profiles.all
labels = profiles.map do |profile|
security = profile.tls ? _("TLS") : _("plain")
_("%{name}, %{host}, port %{port}, %{security}") % {
:name => profile.name,
:host => profile.host,
:port => profile.port,
:security => security
}
end
list = ListBox.new(
labels,
:header => _("Connections"),
:empty_label => _("No connection profiles. Choose New to create one.")
)
connect_button = Button.new(_("Connect"))
new_button = Button.new(_("New"))
edit_button = Button.new(_("Edit"))
delete_button = Button.new(_("Delete"))
exit_button = Button.new(_("Exit"))
form = Form.new([list, connect_button, new_button, edit_button, delete_button, exit_button], :quiet => true)
form.accept_button = connect_button
form.cancel_button = exit_button
result = nil
choose = proc do |action|
if profiles.empty? && [:connect, :edit, :delete].include?(action)
alert(_("Create a connection profile first."))
else
result = [action, profiles[list.index]]
form.resume
end
end
connect_button.on(:press) { choose.call(:connect) }
new_button.on(:press) do
result = [:new, nil]
form.resume
end
edit_button.on(:press) { choose.call(:edit) }
delete_button.on(:press) { choose.call(:delete) }
exit_button.on(:press) do
result = [:exit, nil]
form.resume
end
form.wait
result || [:exit, nil]
end
def edit_profile(existing)
profile = existing || @profiles.build
name = EditBox.new(_("Profile name"), :text => profile.name)
host = EditBox.new(_("Server host"), :text => profile.host)
port = EditBox.new(_("Port"), :type => EditBox::Flags::Numbers, :text => profile.port.to_s)
tls = CheckBox.new(_("Use TLS"), :checked => profile.tls)
auto_read = CheckBox.new(_("Read incoming text automatically"), :checked => profile.auto_read)
encoding_names = ProfileRepository::ENCODINGS.keys
encoding_index = [ProfileRepository::ENCODINGS.values.index(profile.encoding) || 0, 0].max
encoding = ChoiceListBox.new(
[[_("Character encoding"), encoding_names, encoding_index]],
:header => _("Connection options")
)
save = Button.new(_("Save"))
cancel = Button.new(_("Cancel"))
form = Form.new([name, host, port, tls, auto_read, encoding, save, cancel], :quiet => true)
form.accept_button = save
form.cancel_button = cancel
saved = false
save.on(:press) do
candidate = @profiles.build(
:id => profile.id,
:name => name.text,
:host => host.text,
:port => port.text,
:tls => tls.checked,
:encoding => ProfileRepository::ENCODINGS[encoding_names[encoding.value(0)]],
:auto_read => auto_read.checked
)
errors = @profiles.validate(candidate)
if errors.empty?
@profiles.save(candidate)
saved = true
form.resume
else
alert(errors.join("\n"))
end
end
cancel.on(:press) { form.resume }
form.wait
speak(_("Connection profile saved.")) if saved
rescue Exception => error
Log.error("Meow profile editor failed: #{error.class}: #{error.message}") if defined?(Log)
alert(_("Could not save the connection profile: %{message}") % {:message => error.message})
end
def delete_profile(profile)
return if profile == nil
return unless confirm(_("Delete connection %{name}?") % {:name => profile.name})
@profiles.delete(profile.id)
speak(_("Connection profile deleted."))
rescue Exception => error
alert(_("Could not delete the connection profile: %{message}") % {:message => error.message})
end
def connect(profile)
transport = begin
EltenAPI::Tasks.run(
:title => _("Connecting to %{name}") % {:name => profile.name},
:timeout => 20
) do |_progress, token|
token.raise_if_cancelled!
Transport.connect(profile)
end
rescue EltenAPI::Tasks::Cancelled
speak(_("Connection cancelled."))
return
rescue EltenAPI::Tasks::TimedOut
alert(_("The connection attempt timed out."))
return
rescue SocketError, SystemCallError, IOError, OpenSSL::SSL::SSLError, EltenAPI::TLS::Error => error
alert(_("Could not connect: %{message}") % {:message => error.message})
return
rescue Exception => error
Log.error("Meow connection failed: #{error.class}: #{error.message}") if defined?(Log)
alert(_("Could not connect: %{message}") % {:message => error.message})
return
end
@program.manage(transport)
begin
Session.new(@program, profile, transport).run
rescue Exception => error
Log.error("Meow session failed: #{error.class}: #{error.message}") if defined?(Log)
alert(_("The session closed because of an error: %{message}") % {:message => error.message})
ensure
transport.close
@program.release(transport, :close => false)
end
end
end
end