terminalwire 0.1.0 → 0.1.1
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 +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
|