terminalwire 0.1.0 → 0.1.1

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