lib/terminalwire.rb in terminalwire-0.1.0 vs lib/terminalwire.rb in terminalwire-0.1.1
- old
+ new
@@ -21,120 +21,19 @@
module Terminalwire
class Error < StandardError; end
Loader = Zeitwerk::Loader.for_gem.tap do |loader|
+ loader.ignore("#{__dir__}/generators")
loader.setup
end
module Logging
DEVICE = Logger.new($stdout, level: ENV.fetch("LOG_LEVEL", "info"))
def logger = DEVICE
end
- module Thor
- class Shell < ::Thor::Shell::Basic
- extend Forwardable
-
- # Encapsulates all of the IO devices for a Terminalwire connection.
- attr_reader :session
-
- def_delegators :@session, :stdin, :stdout, :stderr
-
- def initialize(session)
- @session = session
- super()
- end
- end
-
- def self.included(base)
- base.extend ClassMethods
-
- # I have to do this in a block to deal with some of Thor's DSL
- base.class_eval do
- extend Forwardable
-
- protected
-
- no_commands do
- def_delegators :shell, :session
- def_delegators :session, :stdout, :stdin, :stderr, :browser
- def_delegators :stdout, :puts, :print
- def_delegators :stdin, :gets
- end
- end
- end
-
- module ClassMethods
- def start(given_args = ARGV, config = {})
- session = config.delete(:session)
- config[:shell] = Shell.new(session) if session
- super(given_args, config)
- end
- end
- end
-
- module Transport
- class Base
- def initialize
- raise NotImplementedError, "This is an abstract base class"
- end
-
- def read
- raise NotImplementedError, "Subclass must implement #read"
- end
-
- def write(data)
- raise NotImplementedError, "Subclass must implement #write"
- end
-
- def close
- raise NotImplementedError, "Subclass must implement #close"
- end
- end
-
- class WebSocket
- def initialize(websocket)
- @websocket = websocket
- end
-
- def read
- @websocket.read&.buffer
- end
-
- def write(data)
- @websocket.write(data)
- end
-
- def close
- @websocket.close
- end
- end
-
- class Socket < Base
- def initialize(socket)
- @socket = socket
- end
-
- def read
- length = @socket.read(4)
- return nil if length.nil?
- length = length.unpack('L>')[0]
- @socket.read(length)
- end
-
- def write(data)
- length = [data.bytesize].pack('L>')
- @socket.write(length + data)
- end
-
- def close
- @socket.close
- end
- end
- end
-
class Connection
include Logging
attr_reader :transport
@@ -198,454 +97,9 @@
end
def self.protocol_key
name.split("::").last.downcase
end
- end
- end
-
- module Client
- module Resource
- class IO < Terminalwire::Resource::Base
- def dispatch(action, data)
- if @device.respond_to?(action)
- respond @device.public_send(action, data)
- else
- raise "Unknown action #{action} for device ID #{@id}"
- end
- end
- end
-
- class STDOUT < IO
- def connect
- @device = $stdout
- end
- end
-
- class STDIN < IO
- def connect
- @device = $stdin
- end
-
- def dispatch(action, data)
- respond case action
- when "puts"
- @device.puts(data)
- when "gets"
- @device.gets
- when "getpass"
- @device.getpass
- end
- end
- end
-
- class STDERR < IO
- def connect
- @device = $stderr
- end
- end
-
- class File < Terminalwire::Resource::Base
- def connect
- @files = {}
- end
-
- def dispatch(action, data)
- respond case action
- when "read"
- read_file(data)
- when "write"
- write_file(data.fetch(:path), data.fetch(:content))
- when "append"
- append_to_file(data.fetch(:path), data.fetch(:content))
- when "mkdir"
- mkdir(data.fetch(:path))
- when "exist"
- exist?(data.fetch(:path))
- else
- raise "Unknown action #{action} for file device"
- end
- end
-
- def mkdir(path)
- FileUtils.mkdir_p(::File.expand_path(path))
- end
-
- def exist?(path)
- ::File.exist? ::File.expand_path(path)
- end
-
- def read_file(path)
- ::File.read ::File.expand_path(path)
- end
-
- def write_file(path, content)
- ::File.open(::File.expand_path(path), "w") { |f| f.write(content) }
- end
-
- def append_to_file(path, content)
- ::File.open(::File.expand_path(path), "a") { |f| f.write(content) }
- end
-
- def disconnect
- @files.clear
- end
- end
-
- class Browser < Terminalwire::Resource::Base
- def dispatch(action, data)
- respond case action
- when "launch"
- Launchy.open(data)
- "Launched browser with URL: #{data}"
- else
- raise "Unknown action #{action} for browser device"
- end
- end
- end
- end
-
- class ResourceMapper
- def initialize(connection, resources)
- @connection = connection
- @resources = resources
- @devices = Hash.new { |h,k| h[Integer(k)] }
- end
-
- def connect_device(id, type)
- klass = @resources.find(type)
- if klass
- device = klass.new(id, @connection)
- device.connect
- @devices[id] = device
- @connection.write(event: "device", action: "connect", status: "success", id: id, type: type)
- else
- @connection.write(event: "device", action: "connect", status: "failure", id: id, type: type, message: "Unknown device type")
- end
- end
-
- def dispatch(id, action, data)
- device = @devices[id]
- if device
- device.dispatch(action, data)
- else
- raise "Unknown device ID: #{id}"
- end
- end
-
- def disconnect_device(id)
- device = @devices.delete(id)
- device&.disconnect
- @connection.write(event: "device", action: "disconnect", id: id)
- end
- end
-
- class Handler
- include Logging
-
- def initialize(connection, resources = self.class.resources)
- @connection = connection
- @resources = resources
- end
-
- def connect
- @devices = ResourceMapper.new(@connection, @resources)
-
- @connection.write(event: "initialize", protocol: { version: "0.1.0" }, arguments: ARGV, program_name: $0)
-
- loop do
- handle @connection.recv
- end
- end
-
- def handle(message)
- case message
- in { event: "device", action: "connect", id:, type: }
- @devices.connect_device(id, type)
- in { event: "device", action: "command", id:, command:, data: }
- @devices.dispatch(id, command, data)
- in { event: "device", action: "disconnect", id: }
- @devices.disconnect_device(id)
- in { event: "exit", status: }
- exit Integer(status)
- end
- end
-
- def self.resources
- ResourceRegistry.new.tap do |resources|
- resources << Client::Resource::STDOUT
- resources << Client::Resource::STDIN
- resources << Client::Resource::STDERR
- resources << Client::Resource::Browser
- resources << Client::Resource::File
- end
- end
- end
-
- def self.tcp(...)
- socket = TCPSocket.new(...)
- transport = Terminalwire::Transport::Socket.new(socket)
- connection = Terminalwire::Connection.new(transport)
- Terminalwire::Client::Handler.new(connection)
- end
-
- def self.socket(...)
- socket = UNIXSocket.new(...)
- transport = Terminalwire::Transport::Socket.new(socket)
- connection = Terminalwire::Connection.new(transport)
- Terminalwire::Client::Handler.new(connection)
- end
-
- def self.websocket(url)
- url = URI(url)
-
- Async do |task|
- endpoint = Async::HTTP::Endpoint.parse(url)
-
- Async::WebSocket::Client.connect(endpoint) do |connection|
- transport = Terminalwire::Transport::WebSocket.new(connection)
- connection = Terminalwire::Connection.new(transport)
- Terminalwire::Client::Handler.new(connection).connect
- end
- end
- end
- end
-
- module Server
- module Resource
- class IO < Terminalwire::Resource::Base
- def puts(data)
- command("puts", data: data)
- end
-
- def print(data)
- command("print", data: data)
- end
-
- def gets
- command("gets")
- end
-
- def flush
- # @connection.flush
- end
-
- private
-
- def command(command, data: nil)
- @connection.write(event: "device", id: @id, action: "command", command: command, data: data)
- @connection.recv&.fetch(:response)
- end
- end
-
- class STDOUT < IO
- end
-
- class STDIN < IO
- def getpass
- command("getpass")
- end
- end
-
- class STDERR < IO
- end
-
- class File < Terminalwire::Resource::Base
- def read(path)
- command("read", path.to_s)
- end
-
- def write(path, content)
- command("write", { 'path' => path.to_s, 'content' => content })
- end
-
- def append(path, content)
- command("append", { 'path' => path.to_s, 'content' => content })
- end
-
- def mkdir(path)
- command("mkdir", { 'path' => path.to_s })
- end
-
- def exist?(path)
- command("exist", { 'path' => path.to_s })
- end
-
- private
-
- def command(action, data)
- @connection.write(event: "device", id: @id, action: "command", command: action, data: data)
- response = @connection.recv
- response.fetch(:response)
- end
- end
-
- class Browser < Terminalwire::Resource::Base
- def launch(url)
- command("launch", data: url)
- end
-
- private
-
- def command(command, data: nil)
- @connection.write(event: "device", id: @id, action: "command", command: command, data: data)
- @connection.recv.fetch(:response)
- end
- end
- end
-
- class ResourceMapper
- include Logging
-
- def initialize(connection, resources = self.class.resources)
- @id = -1
- @resources = resources
- @devices = Hash.new { |h,k| h[Integer(k)] }
- @connection = connection
- end
-
- def connect_device(type)
- id = next_id
- logger.debug "Server: Requesting client to connect device #{type} with ID #{id}"
- @connection.write(event: "device", action: "connect", id: id, type: type)
- response = @connection.recv
- case response
- in { status: "success" }
- logger.debug "Server: Resource #{type} connected with ID #{id}."
- @devices[id] = @resources.find(type).new(id, @connection)
- else
- logger.debug "Server: Failed to connect device #{type} with ID #{id}."
- end
- end
-
- private
-
- def next_id
- @id += 1
- end
-
- def self.resources
- ResourceRegistry.new.tap do |resources|
- resources << Server::Resource::STDOUT
- resources << Server::Resource::STDIN
- resources << Server::Resource::STDERR
- resources << Server::Resource::Browser
- resources << Server::Resource::File
- end
- end
- end
-
- class Session
- extend Forwardable
-
- attr_reader :stdout, :stdin, :stderr, :browser, :file
-
- def_delegators :@stdout, :puts, :print
- def_delegators :@stdin, :gets, :getpass
-
- def initialize(connection:)
- @connection = connection
- @devices = ResourceMapper.new(@connection)
- @stdout = @devices.connect_device("stdout")
- @stdin = @devices.connect_device("stdin")
- @stderr = @devices.connect_device("stderr")
- @browser = @devices.connect_device("browser")
- @file = @devices.connect_device("file")
-
- if block_given?
- begin
- yield self
- ensure
- exit
- end
- end
- end
-
- def exec(&shell)
- instance_eval(&shell)
- ensure
- exit
- end
-
- def exit(status = 0)
- @connection.write(event: "exit", status: status)
- end
-
- def close
- @connection.close
- end
- end
-
- class MyCLI < ::Thor
- include Terminalwire::Thor
-
- desc "greet NAME", "Greet a person"
- def greet(name)
- name = ask "What's your name?"
- say "Hello, #{name}!"
- end
- end
-
- class Socket
- include Logging
-
- def initialize(server_socket)
- @server_socket = server_socket
- end
-
- def listen
- logger.info "Socket: Sistening..."
- loop do
- client_socket = @server_socket.accept
- logger.debug "Socket: Client #{client_socket.inspect} connected"
- handle_client(client_socket)
- end
- end
-
- private
-
- def handle_client(socket)
- transport = Transport::Socket.new(socket)
- connection = Connection.new(transport)
-
- Thread.new do
- handler = Handler.new(connection)
- handler.run
- end
- end
- end
-
- class Handler
- include Logging
-
- def initialize(connection)
- @connection = connection
- end
-
- def run
- logger.info "Server Handler: Running"
- loop do
- message = @connection.recv
- case message
- in { event: "initialize", arguments:, program_name: }
- Session.new(connection: @connection) do |session|
- MyCLI.start(arguments, session: session)
- end
- end
- end
- rescue EOFError, Errno::ECONNRESET
- logger.info "Server Handler: Client disconnected"
- ensure
- @connection.close
- end
- end
-
- def self.tcp(...)
- Server::Socket.new(TCPServer.new(...))
- end
-
- def self.socket(...)
- Server::Socket.new(UNIXServer.new(...))
end
end
module WebSocket
class Server
\ No newline at end of file