terminalwire 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,232 @@
1
+ module Terminalwire
2
+ module Server
3
+ module Resource
4
+ class Base < Terminalwire::Resource::Base
5
+ private
6
+
7
+ def command(command, **parameters)
8
+ @adapter.write(
9
+ event: "resource",
10
+ name: @name,
11
+ action: "command",
12
+ command: command,
13
+ parameters: parameters
14
+ )
15
+ @adapter.recv&.fetch(:response)
16
+ end
17
+ end
18
+
19
+ class STDOUT < Base
20
+ def puts(data)
21
+ command("print_line", data: data)
22
+ end
23
+
24
+ def print(data)
25
+ command("print", data: data)
26
+ end
27
+
28
+ def flush
29
+ # Do nothing
30
+ end
31
+ end
32
+
33
+ class STDERR < STDOUT
34
+ end
35
+
36
+ class STDIN < Base
37
+ def getpass
38
+ command("read_password")
39
+ end
40
+
41
+ def gets
42
+ command("read_line")
43
+ end
44
+ end
45
+
46
+ class File < Base
47
+ def read(path)
48
+ command("read", path: path.to_s)
49
+ end
50
+
51
+ def write(path, content)
52
+ command("write", path: path.to_s, content:)
53
+ end
54
+
55
+ def append(path, content)
56
+ command("append", path: path.to_s, content:)
57
+ end
58
+
59
+ def mkdir(path)
60
+ command("mkdir", path: path.to_s)
61
+ end
62
+
63
+ def delete(path)
64
+ command("delete", path: path.to_s)
65
+ end
66
+
67
+ def exist?(path)
68
+ command("exist", path: path.to_s)
69
+ end
70
+ end
71
+
72
+ class Browser < Base
73
+ def launch(url)
74
+ command("launch", url: url)
75
+ end
76
+ end
77
+ end
78
+
79
+ class Context
80
+ extend Forwardable
81
+
82
+ attr_reader :stdout, :stdin, :stderr, :browser, :file, :storage_path
83
+
84
+ def_delegators :@stdout, :puts, :print
85
+ def_delegators :@stdin, :gets, :getpass
86
+
87
+ def initialize(adapter:, entitlement:)
88
+ @adapter = adapter
89
+
90
+ # TODO: Encapsulate entitlement in a class instead of a hash.
91
+ @entitlement = entitlement
92
+ @storage_path = Pathname.new(entitlement.fetch(:storage_path))
93
+
94
+ @stdout = Server::Resource::STDOUT.new("stdout", @adapter)
95
+ @stdin = Server::Resource::STDIN.new("stdin", @adapter)
96
+ @stderr = Server::Resource::STDERR.new("stderr", @adapter)
97
+ @browser = Server::Resource::Browser.new("browser", @adapter)
98
+ @file = Server::Resource::File.new("file", @adapter)
99
+
100
+ if block_given?
101
+ begin
102
+ yield self
103
+ ensure
104
+ exit
105
+ end
106
+ end
107
+ end
108
+
109
+ def exit(status = 0)
110
+ @adapter.write(event: "exit", status: status)
111
+ end
112
+
113
+ def close
114
+ @adapter.close
115
+ end
116
+ end
117
+
118
+ class MyCLI < ::Thor
119
+ include Terminalwire::Thor
120
+
121
+ desc "greet NAME", "Greet a person"
122
+ def greet(name)
123
+ name = ask "What's your name?"
124
+ say "Hello, #{name}!"
125
+ end
126
+ end
127
+
128
+ class Socket
129
+ include Logging
130
+
131
+ def initialize(server_socket)
132
+ @server_socket = server_socket
133
+ end
134
+
135
+ def listen
136
+ logger.info "Socket: Listening..."
137
+ loop do
138
+ client_socket = @server_socket.accept
139
+ logger.debug "Socket: Client #{client_socket.inspect} connected"
140
+ handle_client(client_socket)
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def handle_client(socket)
147
+ transport = Transport::Socket.new(socket)
148
+ adapter = Adapter.new(transport)
149
+
150
+ Thread.new do
151
+ handler = Handler.new(adapter)
152
+ handler.run
153
+ end
154
+ end
155
+ end
156
+
157
+ class WebSocket
158
+ include Logging
159
+
160
+ def call(env)
161
+ Async::WebSocket::Adapters::Rack.open(env, protocols: ['ws']) do |connection|
162
+ run(Adapter.new(Terminalwire::Transport::WebSocket.new(connection)))
163
+ end or [200, { "Content-Type" => "text/plain" }, ["Connect via WebSockets"]]
164
+ end
165
+
166
+ private
167
+
168
+ def run(adapter)
169
+ while message = adapter.recv
170
+ puts message
171
+ end
172
+ end
173
+ end
174
+
175
+ class Thor < WebSocket
176
+ include Logging
177
+
178
+ def initialize(cli_class)
179
+ @cli_class = cli_class
180
+
181
+ unless @cli_class.included_modules.include?(Terminalwire::Thor)
182
+ raise 'Add `include Terminalwire::Thor` to the #{@cli_class.inspect} class.'
183
+ end
184
+ end
185
+
186
+ def run(adapter)
187
+ logger.info "ThorServer: Running #{@cli_class.inspect}"
188
+ while message = adapter.recv
189
+ case message
190
+ in { event: "initialization", protocol:, program: { arguments: }, entitlement: }
191
+ Terminalwire::Server::Context.new(adapter:, entitlement:) do |context|
192
+ @cli_class.start(arguments, context:)
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ class Handler
200
+ include Logging
201
+
202
+ def initialize(adapter)
203
+ @adapter = adapter
204
+ end
205
+
206
+ def run
207
+ logger.info "Server Handler: Running"
208
+ loop do
209
+ message = @adapter.recv
210
+ case message
211
+ in { event: "initialization", protocol:, program: { arguments: }, entitlement: }
212
+ Context.new(adapter: @adapter) do |context|
213
+ MyCLI.start(arguments, context:)
214
+ end
215
+ end
216
+ end
217
+ rescue EOFError, Errno::ECONNRESET
218
+ logger.info "Server Handler: Client disconnected"
219
+ ensure
220
+ @adapter.close
221
+ end
222
+ end
223
+
224
+ def self.tcp(...)
225
+ Server::Socket.new(TCPServer.new(...))
226
+ end
227
+
228
+ def self.socket(...)
229
+ Server::Socket.new(UNIXServer.new(...))
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,51 @@
1
+ require 'thor'
2
+
3
+ module Terminalwire
4
+ module Thor
5
+ class Shell < ::Thor::Shell::Basic
6
+ extend Forwardable
7
+
8
+ # Encapsulates all of the IO resources for a Terminalwire adapter.
9
+ attr_reader :context, :session
10
+
11
+ def_delegators :context,
12
+ :stdin, :stdout, :stderr
13
+
14
+ def initialize(context, *, **, &)
15
+ @context = context
16
+ @session = Terminalwire::Rails::Session.new(context:)
17
+ super(*,**,&)
18
+ end
19
+ end
20
+
21
+ def self.included(base)
22
+ base.extend ClassMethods
23
+
24
+ # I have to do this in a block to deal with some of Thor's DSL
25
+ base.class_eval do
26
+ extend Forwardable
27
+
28
+ protected
29
+
30
+ no_commands do
31
+ def_delegators :shell,
32
+ :context, :session
33
+ def_delegators :context,
34
+ :stdout, :stdin, :stderr, :browser
35
+ def_delegators :stdout,
36
+ :puts, :print
37
+ def_delegators :stdin,
38
+ :gets
39
+ end
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+ def start(given_args = ARGV, config = {})
45
+ context = config.delete(:context)
46
+ config[:shell] = Shell.new(context) if context
47
+ super(given_args, config)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,123 @@
1
+ require 'uri'
2
+ require 'socket'
3
+ require 'async/websocket/client'
4
+
5
+ module Terminalwire
6
+ module Transport
7
+ class Base
8
+ def self.connect(url)
9
+ raise NotImplementedError, "Subclass must implement .connect"
10
+ end
11
+
12
+ def self.listen(url)
13
+ raise NotImplementedError, "Subclass must implement .listen"
14
+ end
15
+
16
+ def read
17
+ raise NotImplementedError, "Subclass must implement #read"
18
+ end
19
+
20
+ def write(data)
21
+ raise NotImplementedError, "Subclass must implement #write"
22
+ end
23
+
24
+ def close
25
+ raise NotImplementedError, "Subclass must implement #close"
26
+ end
27
+ end
28
+
29
+ class TCP < Base
30
+ def self.connect(url)
31
+ uri = URI(url)
32
+ new(TCPSocket.new(uri.host, uri.port))
33
+ end
34
+
35
+ def self.listen(url)
36
+ uri = URI(url)
37
+ new(TCPServer.new(uri.host, uri.port))
38
+ end
39
+
40
+ def initialize(socket)
41
+ @socket = socket
42
+ end
43
+
44
+ def read
45
+ length = @socket.read(4)
46
+ return nil if length.nil?
47
+ length = length.unpack('L>')[0]
48
+ @socket.read(length)
49
+ end
50
+
51
+ def write(data)
52
+ length = [data.bytesize].pack('L>')
53
+ @socket.write(length + data)
54
+ end
55
+
56
+ def close
57
+ @socket.close
58
+ end
59
+ end
60
+
61
+ class Unix < Base
62
+ def self.connect(url)
63
+ uri = URI(url)
64
+ new(UNIXSocket.new(uri.path))
65
+ end
66
+
67
+ def self.listen(url)
68
+ uri = URI(url)
69
+ new(UNIXServer.new(uri.path))
70
+ end
71
+
72
+ def initialize(socket)
73
+ @socket = socket
74
+ end
75
+
76
+ def read
77
+ length = @socket.read(4)
78
+ return nil if length.nil?
79
+ length = length.unpack('L>')[0]
80
+ @socket.read(length)
81
+ end
82
+
83
+ def write(data)
84
+ length = [data.bytesize].pack('L>')
85
+ @socket.write(length + data)
86
+ end
87
+
88
+ def close
89
+ @socket.close
90
+ end
91
+ end
92
+
93
+ class WebSocket < Base
94
+ def self.connect(url)
95
+ uri = URI(url)
96
+ endpoint = Async::HTTP::Endpoint.parse(uri)
97
+ adapter = Async::WebSocket::Client.connect(endpoint)
98
+ new(adapter)
99
+ end
100
+
101
+ def self.listen(url)
102
+ # This would need to be implemented with a WebSocket server library
103
+ raise NotImplementedError, "WebSocket server not implemented"
104
+ end
105
+
106
+ def initialize(websocket)
107
+ @websocket = websocket
108
+ end
109
+
110
+ def read
111
+ @websocket.read&.buffer
112
+ end
113
+
114
+ def write(data)
115
+ @websocket.write(data)
116
+ end
117
+
118
+ def close
119
+ @websocket.close
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terminalwire
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end