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.
@@ -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