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