terminalwire 0.1.0 → 0.1.1
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 +36 -0
- data/lib/generators/terminalwire/install/templates/bin/terminalwire +2 -0
- data/lib/terminalwire/client/binary.rb +35 -0
- data/lib/terminalwire/client.rb +213 -0
- data/lib/terminalwire/server.rb +239 -0
- data/lib/terminalwire/thor.rb +43 -0
- data/lib/terminalwire/transport.rb +61 -0
- data/lib/terminalwire/version.rb +1 -1
- data/lib/terminalwire.rb +1 -547
- metadata +59 -5
@@ -0,0 +1,61 @@
|
|
1
|
+
module Terminalwire
|
2
|
+
module Transport
|
3
|
+
class Base
|
4
|
+
def initialize
|
5
|
+
raise NotImplementedError, "This is an abstract base class"
|
6
|
+
end
|
7
|
+
|
8
|
+
def read
|
9
|
+
raise NotImplementedError, "Subclass must implement #read"
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(data)
|
13
|
+
raise NotImplementedError, "Subclass must implement #write"
|
14
|
+
end
|
15
|
+
|
16
|
+
def close
|
17
|
+
raise NotImplementedError, "Subclass must implement #close"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class WebSocket
|
22
|
+
def initialize(websocket)
|
23
|
+
@websocket = websocket
|
24
|
+
end
|
25
|
+
|
26
|
+
def read
|
27
|
+
@websocket.read&.buffer
|
28
|
+
end
|
29
|
+
|
30
|
+
def write(data)
|
31
|
+
@websocket.write(data)
|
32
|
+
end
|
33
|
+
|
34
|
+
def close
|
35
|
+
@websocket.close
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Socket < Base
|
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
|
+
end
|
61
|
+
end
|
data/lib/terminalwire/version.rb
CHANGED
data/lib/terminalwire.rb
CHANGED
@@ -23,6 +23,7 @@ module Terminalwire
|
|
23
23
|
class Error < StandardError; end
|
24
24
|
|
25
25
|
Loader = Zeitwerk::Loader.for_gem.tap do |loader|
|
26
|
+
loader.ignore("#{__dir__}/generators")
|
26
27
|
loader.setup
|
27
28
|
end
|
28
29
|
|
@@ -31,108 +32,6 @@ module Terminalwire
|
|
31
32
|
def logger = DEVICE
|
32
33
|
end
|
33
34
|
|
34
|
-
module Thor
|
35
|
-
class Shell < ::Thor::Shell::Basic
|
36
|
-
extend Forwardable
|
37
|
-
|
38
|
-
# Encapsulates all of the IO devices for a Terminalwire connection.
|
39
|
-
attr_reader :session
|
40
|
-
|
41
|
-
def_delegators :@session, :stdin, :stdout, :stderr
|
42
|
-
|
43
|
-
def initialize(session)
|
44
|
-
@session = session
|
45
|
-
super()
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.included(base)
|
50
|
-
base.extend ClassMethods
|
51
|
-
|
52
|
-
# I have to do this in a block to deal with some of Thor's DSL
|
53
|
-
base.class_eval do
|
54
|
-
extend Forwardable
|
55
|
-
|
56
|
-
protected
|
57
|
-
|
58
|
-
no_commands do
|
59
|
-
def_delegators :shell, :session
|
60
|
-
def_delegators :session, :stdout, :stdin, :stderr, :browser
|
61
|
-
def_delegators :stdout, :puts, :print
|
62
|
-
def_delegators :stdin, :gets
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
module ClassMethods
|
68
|
-
def start(given_args = ARGV, config = {})
|
69
|
-
session = config.delete(:session)
|
70
|
-
config[:shell] = Shell.new(session) if session
|
71
|
-
super(given_args, config)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
module Transport
|
77
|
-
class Base
|
78
|
-
def initialize
|
79
|
-
raise NotImplementedError, "This is an abstract base class"
|
80
|
-
end
|
81
|
-
|
82
|
-
def read
|
83
|
-
raise NotImplementedError, "Subclass must implement #read"
|
84
|
-
end
|
85
|
-
|
86
|
-
def write(data)
|
87
|
-
raise NotImplementedError, "Subclass must implement #write"
|
88
|
-
end
|
89
|
-
|
90
|
-
def close
|
91
|
-
raise NotImplementedError, "Subclass must implement #close"
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
class WebSocket
|
96
|
-
def initialize(websocket)
|
97
|
-
@websocket = websocket
|
98
|
-
end
|
99
|
-
|
100
|
-
def read
|
101
|
-
@websocket.read&.buffer
|
102
|
-
end
|
103
|
-
|
104
|
-
def write(data)
|
105
|
-
@websocket.write(data)
|
106
|
-
end
|
107
|
-
|
108
|
-
def close
|
109
|
-
@websocket.close
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
class Socket < Base
|
114
|
-
def initialize(socket)
|
115
|
-
@socket = socket
|
116
|
-
end
|
117
|
-
|
118
|
-
def read
|
119
|
-
length = @socket.read(4)
|
120
|
-
return nil if length.nil?
|
121
|
-
length = length.unpack('L>')[0]
|
122
|
-
@socket.read(length)
|
123
|
-
end
|
124
|
-
|
125
|
-
def write(data)
|
126
|
-
length = [data.bytesize].pack('L>')
|
127
|
-
@socket.write(length + data)
|
128
|
-
end
|
129
|
-
|
130
|
-
def close
|
131
|
-
@socket.close
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
35
|
class Connection
|
137
36
|
include Logging
|
138
37
|
|
@@ -203,451 +102,6 @@ module Terminalwire
|
|
203
102
|
end
|
204
103
|
end
|
205
104
|
|
206
|
-
module Client
|
207
|
-
module Resource
|
208
|
-
class IO < Terminalwire::Resource::Base
|
209
|
-
def dispatch(action, data)
|
210
|
-
if @device.respond_to?(action)
|
211
|
-
respond @device.public_send(action, data)
|
212
|
-
else
|
213
|
-
raise "Unknown action #{action} for device ID #{@id}"
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
class STDOUT < IO
|
219
|
-
def connect
|
220
|
-
@device = $stdout
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
class STDIN < IO
|
225
|
-
def connect
|
226
|
-
@device = $stdin
|
227
|
-
end
|
228
|
-
|
229
|
-
def dispatch(action, data)
|
230
|
-
respond case action
|
231
|
-
when "puts"
|
232
|
-
@device.puts(data)
|
233
|
-
when "gets"
|
234
|
-
@device.gets
|
235
|
-
when "getpass"
|
236
|
-
@device.getpass
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
class STDERR < IO
|
242
|
-
def connect
|
243
|
-
@device = $stderr
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
class File < Terminalwire::Resource::Base
|
248
|
-
def connect
|
249
|
-
@files = {}
|
250
|
-
end
|
251
|
-
|
252
|
-
def dispatch(action, data)
|
253
|
-
respond case action
|
254
|
-
when "read"
|
255
|
-
read_file(data)
|
256
|
-
when "write"
|
257
|
-
write_file(data.fetch(:path), data.fetch(:content))
|
258
|
-
when "append"
|
259
|
-
append_to_file(data.fetch(:path), data.fetch(:content))
|
260
|
-
when "mkdir"
|
261
|
-
mkdir(data.fetch(:path))
|
262
|
-
when "exist"
|
263
|
-
exist?(data.fetch(:path))
|
264
|
-
else
|
265
|
-
raise "Unknown action #{action} for file device"
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
def mkdir(path)
|
270
|
-
FileUtils.mkdir_p(::File.expand_path(path))
|
271
|
-
end
|
272
|
-
|
273
|
-
def exist?(path)
|
274
|
-
::File.exist? ::File.expand_path(path)
|
275
|
-
end
|
276
|
-
|
277
|
-
def read_file(path)
|
278
|
-
::File.read ::File.expand_path(path)
|
279
|
-
end
|
280
|
-
|
281
|
-
def write_file(path, content)
|
282
|
-
::File.open(::File.expand_path(path), "w") { |f| f.write(content) }
|
283
|
-
end
|
284
|
-
|
285
|
-
def append_to_file(path, content)
|
286
|
-
::File.open(::File.expand_path(path), "a") { |f| f.write(content) }
|
287
|
-
end
|
288
|
-
|
289
|
-
def disconnect
|
290
|
-
@files.clear
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
class Browser < Terminalwire::Resource::Base
|
295
|
-
def dispatch(action, data)
|
296
|
-
respond case action
|
297
|
-
when "launch"
|
298
|
-
Launchy.open(data)
|
299
|
-
"Launched browser with URL: #{data}"
|
300
|
-
else
|
301
|
-
raise "Unknown action #{action} for browser device"
|
302
|
-
end
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
class ResourceMapper
|
308
|
-
def initialize(connection, resources)
|
309
|
-
@connection = connection
|
310
|
-
@resources = resources
|
311
|
-
@devices = Hash.new { |h,k| h[Integer(k)] }
|
312
|
-
end
|
313
|
-
|
314
|
-
def connect_device(id, type)
|
315
|
-
klass = @resources.find(type)
|
316
|
-
if klass
|
317
|
-
device = klass.new(id, @connection)
|
318
|
-
device.connect
|
319
|
-
@devices[id] = device
|
320
|
-
@connection.write(event: "device", action: "connect", status: "success", id: id, type: type)
|
321
|
-
else
|
322
|
-
@connection.write(event: "device", action: "connect", status: "failure", id: id, type: type, message: "Unknown device type")
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
def dispatch(id, action, data)
|
327
|
-
device = @devices[id]
|
328
|
-
if device
|
329
|
-
device.dispatch(action, data)
|
330
|
-
else
|
331
|
-
raise "Unknown device ID: #{id}"
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
def disconnect_device(id)
|
336
|
-
device = @devices.delete(id)
|
337
|
-
device&.disconnect
|
338
|
-
@connection.write(event: "device", action: "disconnect", id: id)
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
class Handler
|
343
|
-
include Logging
|
344
|
-
|
345
|
-
def initialize(connection, resources = self.class.resources)
|
346
|
-
@connection = connection
|
347
|
-
@resources = resources
|
348
|
-
end
|
349
|
-
|
350
|
-
def connect
|
351
|
-
@devices = ResourceMapper.new(@connection, @resources)
|
352
|
-
|
353
|
-
@connection.write(event: "initialize", protocol: { version: "0.1.0" }, arguments: ARGV, program_name: $0)
|
354
|
-
|
355
|
-
loop do
|
356
|
-
handle @connection.recv
|
357
|
-
end
|
358
|
-
end
|
359
|
-
|
360
|
-
def handle(message)
|
361
|
-
case message
|
362
|
-
in { event: "device", action: "connect", id:, type: }
|
363
|
-
@devices.connect_device(id, type)
|
364
|
-
in { event: "device", action: "command", id:, command:, data: }
|
365
|
-
@devices.dispatch(id, command, data)
|
366
|
-
in { event: "device", action: "disconnect", id: }
|
367
|
-
@devices.disconnect_device(id)
|
368
|
-
in { event: "exit", status: }
|
369
|
-
exit Integer(status)
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
def self.resources
|
374
|
-
ResourceRegistry.new.tap do |resources|
|
375
|
-
resources << Client::Resource::STDOUT
|
376
|
-
resources << Client::Resource::STDIN
|
377
|
-
resources << Client::Resource::STDERR
|
378
|
-
resources << Client::Resource::Browser
|
379
|
-
resources << Client::Resource::File
|
380
|
-
end
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
def self.tcp(...)
|
385
|
-
socket = TCPSocket.new(...)
|
386
|
-
transport = Terminalwire::Transport::Socket.new(socket)
|
387
|
-
connection = Terminalwire::Connection.new(transport)
|
388
|
-
Terminalwire::Client::Handler.new(connection)
|
389
|
-
end
|
390
|
-
|
391
|
-
def self.socket(...)
|
392
|
-
socket = UNIXSocket.new(...)
|
393
|
-
transport = Terminalwire::Transport::Socket.new(socket)
|
394
|
-
connection = Terminalwire::Connection.new(transport)
|
395
|
-
Terminalwire::Client::Handler.new(connection)
|
396
|
-
end
|
397
|
-
|
398
|
-
def self.websocket(url)
|
399
|
-
url = URI(url)
|
400
|
-
|
401
|
-
Async do |task|
|
402
|
-
endpoint = Async::HTTP::Endpoint.parse(url)
|
403
|
-
|
404
|
-
Async::WebSocket::Client.connect(endpoint) do |connection|
|
405
|
-
transport = Terminalwire::Transport::WebSocket.new(connection)
|
406
|
-
connection = Terminalwire::Connection.new(transport)
|
407
|
-
Terminalwire::Client::Handler.new(connection).connect
|
408
|
-
end
|
409
|
-
end
|
410
|
-
end
|
411
|
-
end
|
412
|
-
|
413
|
-
module Server
|
414
|
-
module Resource
|
415
|
-
class IO < Terminalwire::Resource::Base
|
416
|
-
def puts(data)
|
417
|
-
command("puts", data: data)
|
418
|
-
end
|
419
|
-
|
420
|
-
def print(data)
|
421
|
-
command("print", data: data)
|
422
|
-
end
|
423
|
-
|
424
|
-
def gets
|
425
|
-
command("gets")
|
426
|
-
end
|
427
|
-
|
428
|
-
def flush
|
429
|
-
# @connection.flush
|
430
|
-
end
|
431
|
-
|
432
|
-
private
|
433
|
-
|
434
|
-
def command(command, data: nil)
|
435
|
-
@connection.write(event: "device", id: @id, action: "command", command: command, data: data)
|
436
|
-
@connection.recv&.fetch(:response)
|
437
|
-
end
|
438
|
-
end
|
439
|
-
|
440
|
-
class STDOUT < IO
|
441
|
-
end
|
442
|
-
|
443
|
-
class STDIN < IO
|
444
|
-
def getpass
|
445
|
-
command("getpass")
|
446
|
-
end
|
447
|
-
end
|
448
|
-
|
449
|
-
class STDERR < IO
|
450
|
-
end
|
451
|
-
|
452
|
-
class File < Terminalwire::Resource::Base
|
453
|
-
def read(path)
|
454
|
-
command("read", path.to_s)
|
455
|
-
end
|
456
|
-
|
457
|
-
def write(path, content)
|
458
|
-
command("write", { 'path' => path.to_s, 'content' => content })
|
459
|
-
end
|
460
|
-
|
461
|
-
def append(path, content)
|
462
|
-
command("append", { 'path' => path.to_s, 'content' => content })
|
463
|
-
end
|
464
|
-
|
465
|
-
def mkdir(path)
|
466
|
-
command("mkdir", { 'path' => path.to_s })
|
467
|
-
end
|
468
|
-
|
469
|
-
def exist?(path)
|
470
|
-
command("exist", { 'path' => path.to_s })
|
471
|
-
end
|
472
|
-
|
473
|
-
private
|
474
|
-
|
475
|
-
def command(action, data)
|
476
|
-
@connection.write(event: "device", id: @id, action: "command", command: action, data: data)
|
477
|
-
response = @connection.recv
|
478
|
-
response.fetch(:response)
|
479
|
-
end
|
480
|
-
end
|
481
|
-
|
482
|
-
class Browser < Terminalwire::Resource::Base
|
483
|
-
def launch(url)
|
484
|
-
command("launch", data: url)
|
485
|
-
end
|
486
|
-
|
487
|
-
private
|
488
|
-
|
489
|
-
def command(command, data: nil)
|
490
|
-
@connection.write(event: "device", id: @id, action: "command", command: command, data: data)
|
491
|
-
@connection.recv.fetch(:response)
|
492
|
-
end
|
493
|
-
end
|
494
|
-
end
|
495
|
-
|
496
|
-
class ResourceMapper
|
497
|
-
include Logging
|
498
|
-
|
499
|
-
def initialize(connection, resources = self.class.resources)
|
500
|
-
@id = -1
|
501
|
-
@resources = resources
|
502
|
-
@devices = Hash.new { |h,k| h[Integer(k)] }
|
503
|
-
@connection = connection
|
504
|
-
end
|
505
|
-
|
506
|
-
def connect_device(type)
|
507
|
-
id = next_id
|
508
|
-
logger.debug "Server: Requesting client to connect device #{type} with ID #{id}"
|
509
|
-
@connection.write(event: "device", action: "connect", id: id, type: type)
|
510
|
-
response = @connection.recv
|
511
|
-
case response
|
512
|
-
in { status: "success" }
|
513
|
-
logger.debug "Server: Resource #{type} connected with ID #{id}."
|
514
|
-
@devices[id] = @resources.find(type).new(id, @connection)
|
515
|
-
else
|
516
|
-
logger.debug "Server: Failed to connect device #{type} with ID #{id}."
|
517
|
-
end
|
518
|
-
end
|
519
|
-
|
520
|
-
private
|
521
|
-
|
522
|
-
def next_id
|
523
|
-
@id += 1
|
524
|
-
end
|
525
|
-
|
526
|
-
def self.resources
|
527
|
-
ResourceRegistry.new.tap do |resources|
|
528
|
-
resources << Server::Resource::STDOUT
|
529
|
-
resources << Server::Resource::STDIN
|
530
|
-
resources << Server::Resource::STDERR
|
531
|
-
resources << Server::Resource::Browser
|
532
|
-
resources << Server::Resource::File
|
533
|
-
end
|
534
|
-
end
|
535
|
-
end
|
536
|
-
|
537
|
-
class Session
|
538
|
-
extend Forwardable
|
539
|
-
|
540
|
-
attr_reader :stdout, :stdin, :stderr, :browser, :file
|
541
|
-
|
542
|
-
def_delegators :@stdout, :puts, :print
|
543
|
-
def_delegators :@stdin, :gets, :getpass
|
544
|
-
|
545
|
-
def initialize(connection:)
|
546
|
-
@connection = connection
|
547
|
-
@devices = ResourceMapper.new(@connection)
|
548
|
-
@stdout = @devices.connect_device("stdout")
|
549
|
-
@stdin = @devices.connect_device("stdin")
|
550
|
-
@stderr = @devices.connect_device("stderr")
|
551
|
-
@browser = @devices.connect_device("browser")
|
552
|
-
@file = @devices.connect_device("file")
|
553
|
-
|
554
|
-
if block_given?
|
555
|
-
begin
|
556
|
-
yield self
|
557
|
-
ensure
|
558
|
-
exit
|
559
|
-
end
|
560
|
-
end
|
561
|
-
end
|
562
|
-
|
563
|
-
def exec(&shell)
|
564
|
-
instance_eval(&shell)
|
565
|
-
ensure
|
566
|
-
exit
|
567
|
-
end
|
568
|
-
|
569
|
-
def exit(status = 0)
|
570
|
-
@connection.write(event: "exit", status: status)
|
571
|
-
end
|
572
|
-
|
573
|
-
def close
|
574
|
-
@connection.close
|
575
|
-
end
|
576
|
-
end
|
577
|
-
|
578
|
-
class MyCLI < ::Thor
|
579
|
-
include Terminalwire::Thor
|
580
|
-
|
581
|
-
desc "greet NAME", "Greet a person"
|
582
|
-
def greet(name)
|
583
|
-
name = ask "What's your name?"
|
584
|
-
say "Hello, #{name}!"
|
585
|
-
end
|
586
|
-
end
|
587
|
-
|
588
|
-
class Socket
|
589
|
-
include Logging
|
590
|
-
|
591
|
-
def initialize(server_socket)
|
592
|
-
@server_socket = server_socket
|
593
|
-
end
|
594
|
-
|
595
|
-
def listen
|
596
|
-
logger.info "Socket: Sistening..."
|
597
|
-
loop do
|
598
|
-
client_socket = @server_socket.accept
|
599
|
-
logger.debug "Socket: Client #{client_socket.inspect} connected"
|
600
|
-
handle_client(client_socket)
|
601
|
-
end
|
602
|
-
end
|
603
|
-
|
604
|
-
private
|
605
|
-
|
606
|
-
def handle_client(socket)
|
607
|
-
transport = Transport::Socket.new(socket)
|
608
|
-
connection = Connection.new(transport)
|
609
|
-
|
610
|
-
Thread.new do
|
611
|
-
handler = Handler.new(connection)
|
612
|
-
handler.run
|
613
|
-
end
|
614
|
-
end
|
615
|
-
end
|
616
|
-
|
617
|
-
class Handler
|
618
|
-
include Logging
|
619
|
-
|
620
|
-
def initialize(connection)
|
621
|
-
@connection = connection
|
622
|
-
end
|
623
|
-
|
624
|
-
def run
|
625
|
-
logger.info "Server Handler: Running"
|
626
|
-
loop do
|
627
|
-
message = @connection.recv
|
628
|
-
case message
|
629
|
-
in { event: "initialize", arguments:, program_name: }
|
630
|
-
Session.new(connection: @connection) do |session|
|
631
|
-
MyCLI.start(arguments, session: session)
|
632
|
-
end
|
633
|
-
end
|
634
|
-
end
|
635
|
-
rescue EOFError, Errno::ECONNRESET
|
636
|
-
logger.info "Server Handler: Client disconnected"
|
637
|
-
ensure
|
638
|
-
@connection.close
|
639
|
-
end
|
640
|
-
end
|
641
|
-
|
642
|
-
def self.tcp(...)
|
643
|
-
Server::Socket.new(TCPServer.new(...))
|
644
|
-
end
|
645
|
-
|
646
|
-
def self.socket(...)
|
647
|
-
Server::Socket.new(UNIXServer.new(...))
|
648
|
-
end
|
649
|
-
end
|
650
|
-
|
651
105
|
module WebSocket
|
652
106
|
class Server
|
653
107
|
include Logging
|