terminalwire 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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