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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +14 -6
- data/examples/exec/localrails +2 -0
- data/exe/terminalwire-exec +9 -0
- data/lib/generators/terminalwire/install/USAGE +9 -0
- data/lib/generators/terminalwire/install/install_generator.rb +37 -0
- data/lib/generators/terminalwire/install/templates/application_terminal.rb.tt +54 -0
- data/lib/generators/terminalwire/install/templates/bin/terminalwire +2 -0
- data/lib/terminalwire/adapter.rb +32 -0
- data/lib/terminalwire/client/entitlement.rb +107 -0
- data/lib/terminalwire/client/exec.rb +35 -0
- data/lib/terminalwire/client/resource.rb +154 -0
- data/lib/terminalwire/client.rb +91 -0
- data/lib/terminalwire/logging.rb +8 -0
- data/lib/terminalwire/rails.rb +69 -0
- data/lib/terminalwire/server.rb +232 -0
- data/lib/terminalwire/thor.rb +51 -0
- data/lib/terminalwire/transport.rb +123 -0
- data/lib/terminalwire/version.rb +1 -1
- data/lib/terminalwire.rb +14 -655
- metadata +79 -6
@@ -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
|
data/lib/terminalwire/version.rb
CHANGED