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