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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9fec6bb413813bdaa761403bca8fcaebb92b884a448cfe3b5cba7fc56e3589b0
|
4
|
+
data.tar.gz: 319498815fe666a7266f1dcfd2648af1e15477f7f42f22cd8661f2c42bda45c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bbc8104d13cd4acf420c0c5873b4f1df0749ea94768c91a9ceb6100d34c9fe6da0cd62bf4bb717a5c5e5fdc4f3d7395db0a0cd44b5d88cb1e18006dab6c3f791
|
7
|
+
data.tar.gz: daeeb21577a26536a4daad6c61b07f1b6a96a23ea52adfbc4eb25881b2948a1010f81ffc6b4958eb92befc0bae348b18cbecf4aeecea813c0b84b18ea0e33ace
|
data/LICENSE.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
Copyright (c) 2024 Brad Gessler. Email brad@terminalwire.com to discuss
|
1
|
+
Copyright (c) 2024 Brad Gessler. Email brad@terminalwire.com to discuss licensing.
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Terminalwire
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/terminalwire`. To experiment with that code, run `bin/console` for an interactive prompt.
|
3
|
+
Unlike most command-line tools for web services that require an API, Terminalwire streams terminal I/O between a web server and client over WebSockets. This means you can use your preferred command-line parser within your favorite web server framework to deliver a delightful CLI experience to your users.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -14,9 +12,19 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
14
12
|
|
15
13
|
$ gem install terminalwire
|
16
14
|
|
17
|
-
##
|
15
|
+
## Rails
|
16
|
+
|
17
|
+
Run the intallation command:
|
18
|
+
|
19
|
+
$ rails g terminalwire:install my-app
|
20
|
+
|
21
|
+
This generates the `./bin/my-app` file. Run it to verify that it connects to the server.
|
22
|
+
|
23
|
+
$ bin/my-app
|
24
|
+
Commands:
|
25
|
+
my-app help [COMMAND] # Describe available commands or one specific command
|
18
26
|
|
19
|
-
|
27
|
+
To edit the command-line, open `./app/cli/main_cli.rb` and make changes to the `MainCLI` class.
|
20
28
|
|
21
29
|
## Development
|
22
30
|
|
@@ -30,7 +38,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/termin
|
|
30
38
|
|
31
39
|
## License
|
32
40
|
|
33
|
-
The gem is available as a propietary license. Email brad@terminalwire.com to discuss
|
41
|
+
The gem is available as a propietary license. Email brad@terminalwire.com to discuss licensing.
|
34
42
|
|
35
43
|
## Code of Conduct
|
36
44
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "bundler"
|
2
|
+
|
3
|
+
class Terminalwire::InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
argument :binary_name, type: :string, required: true, banner: "binary_name"
|
7
|
+
|
8
|
+
def create_terminal_files
|
9
|
+
template "application_terminal.rb.tt", Rails.root.join("app/terminal/application_terminal.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_binary_files
|
13
|
+
copy_file "bin/terminalwire", binary_path
|
14
|
+
chmod binary_path, 0755, verbose: false
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_route
|
18
|
+
route <<~ROUTE
|
19
|
+
match "/terminal",
|
20
|
+
to: Terminalwire::WebSocket::ThorServer.new(ApplicationTerminal),
|
21
|
+
via: [:get, :connect]
|
22
|
+
ROUTE
|
23
|
+
end
|
24
|
+
|
25
|
+
def print_post_install_message
|
26
|
+
say ""
|
27
|
+
say "Terminalwire has been successfully installed!", :green
|
28
|
+
say "Run `#{binary_path.relative_path_from(Rails.root)}` to verify everything is in working order. For support visit https://terminalwire.com."
|
29
|
+
say ""
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def binary_path
|
35
|
+
Rails.root.join("bin", binary_name)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Learn how to use Thor at http://whatisthor.com.
|
2
|
+
class ApplicationTerminal < Thor
|
3
|
+
include Terminalwire::Thor
|
4
|
+
|
5
|
+
def self.basename = "<%= binary_name %>"
|
6
|
+
|
7
|
+
desc "hello NAME", "say hello to NAME"
|
8
|
+
def hello(name)
|
9
|
+
puts "Hello #{name}"
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "login", "Login to your account"
|
13
|
+
def login
|
14
|
+
print "Email: "
|
15
|
+
email = gets
|
16
|
+
|
17
|
+
print "Password: "
|
18
|
+
password = getch
|
19
|
+
|
20
|
+
if self.current_user = User.authenticate(email, password)
|
21
|
+
puts "Successfully logged in as #{user.email}."
|
22
|
+
else
|
23
|
+
puts "Could not find a user with that email and password."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def current_user=(user)
|
30
|
+
session["user_id"] = user.id
|
31
|
+
end
|
32
|
+
|
33
|
+
def current_user
|
34
|
+
@current_user ||= User.find(session.fetch("user_id"))
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "yaml"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module Terminalwire::Client
|
6
|
+
class Binary
|
7
|
+
attr_reader :arguments, :path, :configuration, :url
|
8
|
+
|
9
|
+
def initialize(path:, arguments:)
|
10
|
+
@arguments = arguments
|
11
|
+
@path = Pathname.new(path)
|
12
|
+
@configuration = YAML.load_file(@path)
|
13
|
+
@url = URI(@configuration.fetch("url"))
|
14
|
+
rescue Errno::ENOENT => e
|
15
|
+
raise Terminalwire::Error, "File not found: #{@path}"
|
16
|
+
rescue URI::InvalidURIError => e
|
17
|
+
raise Terminalwire::Error, "Invalid URI: #{@url}"
|
18
|
+
rescue KeyError => e
|
19
|
+
raise Terminalwire::Error, "Missing key in configuration: #{e}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
Terminalwire::Client.websocket(url:, arguments:)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.execute
|
27
|
+
case ARGV
|
28
|
+
in path, *arguments
|
29
|
+
new(path:, arguments:).start
|
30
|
+
end
|
31
|
+
rescue NoMatchingPatternError => e
|
32
|
+
raise Terminalwire::Error, "Launched with incorrect arguments: #{ARGV}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
module Terminalwire
|
2
|
+
module Client
|
3
|
+
module Resource
|
4
|
+
class IO < Terminalwire::Resource::Base
|
5
|
+
def dispatch(action, data)
|
6
|
+
if @device.respond_to?(action)
|
7
|
+
respond @device.public_send(action, data)
|
8
|
+
else
|
9
|
+
raise "Unknown action #{action} for device ID #{@id}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class STDOUT < IO
|
15
|
+
def connect
|
16
|
+
@device = $stdout
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class STDIN < IO
|
21
|
+
def connect
|
22
|
+
@device = $stdin
|
23
|
+
end
|
24
|
+
|
25
|
+
def dispatch(action, data)
|
26
|
+
respond case action
|
27
|
+
when "puts"
|
28
|
+
@device.puts(data)
|
29
|
+
when "gets"
|
30
|
+
@device.gets
|
31
|
+
when "getpass"
|
32
|
+
@device.getpass
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class STDERR < IO
|
38
|
+
def connect
|
39
|
+
@device = $stderr
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class File < Terminalwire::Resource::Base
|
44
|
+
def connect
|
45
|
+
@files = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def dispatch(action, data)
|
49
|
+
respond case action
|
50
|
+
when "read"
|
51
|
+
read_file(data)
|
52
|
+
when "write"
|
53
|
+
write_file(data.fetch(:path), data.fetch(:content))
|
54
|
+
when "append"
|
55
|
+
append_to_file(data.fetch(:path), data.fetch(:content))
|
56
|
+
when "mkdir"
|
57
|
+
mkdir(data.fetch(:path))
|
58
|
+
when "exist"
|
59
|
+
exist?(data.fetch(:path))
|
60
|
+
else
|
61
|
+
raise "Unknown action #{action} for file device"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def mkdir(path)
|
66
|
+
FileUtils.mkdir_p(::File.expand_path(path))
|
67
|
+
end
|
68
|
+
|
69
|
+
def exist?(path)
|
70
|
+
::File.exist? ::File.expand_path(path)
|
71
|
+
end
|
72
|
+
|
73
|
+
def read_file(path)
|
74
|
+
::File.read ::File.expand_path(path)
|
75
|
+
end
|
76
|
+
|
77
|
+
def write_file(path, content)
|
78
|
+
::File.open(::File.expand_path(path), "w") { |f| f.write(content) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def append_to_file(path, content)
|
82
|
+
::File.open(::File.expand_path(path), "a") { |f| f.write(content) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def disconnect
|
86
|
+
@files.clear
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Browser < Terminalwire::Resource::Base
|
91
|
+
def dispatch(action, data)
|
92
|
+
respond case action
|
93
|
+
when "launch"
|
94
|
+
Launchy.open(data)
|
95
|
+
"Launched browser with URL: #{data}"
|
96
|
+
else
|
97
|
+
raise "Unknown action #{action} for browser device"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class ResourceMapper
|
104
|
+
def initialize(connection, resources)
|
105
|
+
@connection = connection
|
106
|
+
@resources = resources
|
107
|
+
@devices = Hash.new { |h,k| h[Integer(k)] }
|
108
|
+
end
|
109
|
+
|
110
|
+
def connect_device(id, type)
|
111
|
+
klass = @resources.find(type)
|
112
|
+
if klass
|
113
|
+
device = klass.new(id, @connection)
|
114
|
+
device.connect
|
115
|
+
@devices[id] = device
|
116
|
+
@connection.write(event: "device", action: "connect", status: "success", id: id, type: type)
|
117
|
+
else
|
118
|
+
@connection.write(event: "device", action: "connect", status: "failure", id: id, type: type, message: "Unknown device type")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def dispatch(id, action, data)
|
123
|
+
device = @devices[id]
|
124
|
+
if device
|
125
|
+
device.dispatch(action, data)
|
126
|
+
else
|
127
|
+
raise "Unknown device ID: #{id}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def disconnect_device(id)
|
132
|
+
device = @devices.delete(id)
|
133
|
+
device&.disconnect
|
134
|
+
@connection.write(event: "device", action: "disconnect", id: id)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class Handler
|
139
|
+
VERSION = "0.1.0".freeze
|
140
|
+
|
141
|
+
include Logging
|
142
|
+
|
143
|
+
attr_reader :arguments, :program_name
|
144
|
+
|
145
|
+
def initialize(connection, resources = self.class.resources, arguments: ARGV, program_name: $0)
|
146
|
+
@connection = connection
|
147
|
+
@resources = resources
|
148
|
+
@arguments = arguments
|
149
|
+
end
|
150
|
+
|
151
|
+
def connect
|
152
|
+
@devices = ResourceMapper.new(@connection, @resources)
|
153
|
+
|
154
|
+
@connection.write(event: "initialize", protocol: { version: VERSION }, arguments:, program_name:)
|
155
|
+
|
156
|
+
loop do
|
157
|
+
handle @connection.recv
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def handle(message)
|
162
|
+
case message
|
163
|
+
in { event: "device", action: "connect", id:, type: }
|
164
|
+
@devices.connect_device(id, type)
|
165
|
+
in { event: "device", action: "command", id:, command:, data: }
|
166
|
+
@devices.dispatch(id, command, data)
|
167
|
+
in { event: "device", action: "disconnect", id: }
|
168
|
+
@devices.disconnect_device(id)
|
169
|
+
in { event: "exit", status: }
|
170
|
+
exit Integer(status)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.resources
|
175
|
+
ResourceRegistry.new.tap do |resources|
|
176
|
+
resources << Client::Resource::STDOUT
|
177
|
+
resources << Client::Resource::STDIN
|
178
|
+
resources << Client::Resource::STDERR
|
179
|
+
resources << Client::Resource::Browser
|
180
|
+
resources << Client::Resource::File
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.tcp(...)
|
186
|
+
socket = TCPSocket.new(...)
|
187
|
+
transport = Terminalwire::Transport::Socket.new(socket)
|
188
|
+
connection = Terminalwire::Connection.new(transport)
|
189
|
+
Terminalwire::Client::Handler.new(connection)
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.socket(...)
|
193
|
+
socket = UNIXSocket.new(...)
|
194
|
+
transport = Terminalwire::Transport::Socket.new(socket)
|
195
|
+
connection = Terminalwire::Connection.new(transport)
|
196
|
+
Terminalwire::Client::Handler.new(connection)
|
197
|
+
end
|
198
|
+
|
199
|
+
def self.websocket(url:, arguments: ARGV)
|
200
|
+
url = URI(url)
|
201
|
+
|
202
|
+
Async do |task|
|
203
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
204
|
+
|
205
|
+
Async::WebSocket::Client.connect(endpoint) do |connection|
|
206
|
+
transport = Terminalwire::Transport::WebSocket.new(connection)
|
207
|
+
connection = Terminalwire::Connection.new(transport)
|
208
|
+
Terminalwire::Client::Handler.new(connection, arguments:).connect
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
module Terminalwire
|
2
|
+
module Server
|
3
|
+
module Resource
|
4
|
+
class IO < Terminalwire::Resource::Base
|
5
|
+
def puts(data)
|
6
|
+
command("puts", data: data)
|
7
|
+
end
|
8
|
+
|
9
|
+
def print(data)
|
10
|
+
command("print", data: data)
|
11
|
+
end
|
12
|
+
|
13
|
+
def gets
|
14
|
+
command("gets")
|
15
|
+
end
|
16
|
+
|
17
|
+
def flush
|
18
|
+
# @connection.flush
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def command(command, data: nil)
|
24
|
+
@connection.write(event: "device", id: @id, action: "command", command: command, data: data)
|
25
|
+
@connection.recv&.fetch(:response)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class STDOUT < IO
|
30
|
+
end
|
31
|
+
|
32
|
+
class STDIN < IO
|
33
|
+
def getpass
|
34
|
+
command("getpass")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class STDERR < IO
|
39
|
+
end
|
40
|
+
|
41
|
+
class File < Terminalwire::Resource::Base
|
42
|
+
def read(path)
|
43
|
+
command("read", path.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
def write(path, content)
|
47
|
+
command("write", { 'path' => path.to_s, 'content' => content })
|
48
|
+
end
|
49
|
+
|
50
|
+
def append(path, content)
|
51
|
+
command("append", { 'path' => path.to_s, 'content' => content })
|
52
|
+
end
|
53
|
+
|
54
|
+
def mkdir(path)
|
55
|
+
command("mkdir", { 'path' => path.to_s })
|
56
|
+
end
|
57
|
+
|
58
|
+
def exist?(path)
|
59
|
+
command("exist", { 'path' => path.to_s })
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def command(action, data)
|
65
|
+
@connection.write(event: "device", id: @id, action: "command", command: action, data: data)
|
66
|
+
response = @connection.recv
|
67
|
+
response.fetch(:response)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Browser < Terminalwire::Resource::Base
|
72
|
+
def launch(url)
|
73
|
+
command("launch", data: url)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def command(command, data: nil)
|
79
|
+
@connection.write(event: "device", id: @id, action: "command", command: command, data: data)
|
80
|
+
@connection.recv.fetch(:response)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class ResourceMapper
|
86
|
+
include Logging
|
87
|
+
|
88
|
+
def initialize(connection, resources = self.class.resources)
|
89
|
+
@id = -1
|
90
|
+
@resources = resources
|
91
|
+
@devices = Hash.new { |h,k| h[Integer(k)] }
|
92
|
+
@connection = connection
|
93
|
+
end
|
94
|
+
|
95
|
+
def connect_device(type)
|
96
|
+
id = next_id
|
97
|
+
logger.debug "Server: Requesting client to connect device #{type} with ID #{id}"
|
98
|
+
@connection.write(event: "device", action: "connect", id: id, type: type)
|
99
|
+
response = @connection.recv
|
100
|
+
case response
|
101
|
+
in { status: "success" }
|
102
|
+
logger.debug "Server: Resource #{type} connected with ID #{id}."
|
103
|
+
@devices[id] = @resources.find(type).new(id, @connection)
|
104
|
+
else
|
105
|
+
logger.debug "Server: Failed to connect device #{type} with ID #{id}."
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def next_id
|
112
|
+
@id += 1
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.resources
|
116
|
+
ResourceRegistry.new.tap do |resources|
|
117
|
+
resources << Server::Resource::STDOUT
|
118
|
+
resources << Server::Resource::STDIN
|
119
|
+
resources << Server::Resource::STDERR
|
120
|
+
resources << Server::Resource::Browser
|
121
|
+
resources << Server::Resource::File
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class Session
|
127
|
+
extend Forwardable
|
128
|
+
|
129
|
+
attr_reader :stdout, :stdin, :stderr, :browser, :file
|
130
|
+
|
131
|
+
def_delegators :@stdout, :puts, :print
|
132
|
+
def_delegators :@stdin, :gets, :getpass
|
133
|
+
|
134
|
+
def initialize(connection:)
|
135
|
+
@connection = connection
|
136
|
+
@devices = ResourceMapper.new(@connection)
|
137
|
+
@stdout = @devices.connect_device("stdout")
|
138
|
+
@stdin = @devices.connect_device("stdin")
|
139
|
+
@stderr = @devices.connect_device("stderr")
|
140
|
+
@browser = @devices.connect_device("browser")
|
141
|
+
@file = @devices.connect_device("file")
|
142
|
+
|
143
|
+
if block_given?
|
144
|
+
begin
|
145
|
+
yield self
|
146
|
+
ensure
|
147
|
+
exit
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def exec(&shell)
|
153
|
+
instance_eval(&shell)
|
154
|
+
ensure
|
155
|
+
exit
|
156
|
+
end
|
157
|
+
|
158
|
+
def exit(status = 0)
|
159
|
+
@connection.write(event: "exit", status: status)
|
160
|
+
end
|
161
|
+
|
162
|
+
def close
|
163
|
+
@connection.close
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class MyCLI < ::Thor
|
168
|
+
include Terminalwire::Thor
|
169
|
+
|
170
|
+
desc "greet NAME", "Greet a person"
|
171
|
+
def greet(name)
|
172
|
+
name = ask "What's your name?"
|
173
|
+
say "Hello, #{name}!"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class Socket
|
178
|
+
include Logging
|
179
|
+
|
180
|
+
def initialize(server_socket)
|
181
|
+
@server_socket = server_socket
|
182
|
+
end
|
183
|
+
|
184
|
+
def listen
|
185
|
+
logger.info "Socket: Sistening..."
|
186
|
+
loop do
|
187
|
+
client_socket = @server_socket.accept
|
188
|
+
logger.debug "Socket: Client #{client_socket.inspect} connected"
|
189
|
+
handle_client(client_socket)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def handle_client(socket)
|
196
|
+
transport = Transport::Socket.new(socket)
|
197
|
+
connection = Connection.new(transport)
|
198
|
+
|
199
|
+
Thread.new do
|
200
|
+
handler = Handler.new(connection)
|
201
|
+
handler.run
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class Handler
|
207
|
+
include Logging
|
208
|
+
|
209
|
+
def initialize(connection)
|
210
|
+
@connection = connection
|
211
|
+
end
|
212
|
+
|
213
|
+
def run
|
214
|
+
logger.info "Server Handler: Running"
|
215
|
+
loop do
|
216
|
+
message = @connection.recv
|
217
|
+
case message
|
218
|
+
in { event: "initialize", arguments:, program_name: }
|
219
|
+
Session.new(connection: @connection) do |session|
|
220
|
+
MyCLI.start(arguments, session: session)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
rescue EOFError, Errno::ECONNRESET
|
225
|
+
logger.info "Server Handler: Client disconnected"
|
226
|
+
ensure
|
227
|
+
@connection.close
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.tcp(...)
|
232
|
+
Server::Socket.new(TCPServer.new(...))
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.socket(...)
|
236
|
+
Server::Socket.new(UNIXServer.new(...))
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Terminalwire
|
2
|
+
module Thor
|
3
|
+
class Shell < ::Thor::Shell::Basic
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
# Encapsulates all of the IO devices for a Terminalwire connection.
|
7
|
+
attr_reader :session
|
8
|
+
|
9
|
+
def_delegators :@session, :stdin, :stdout, :stderr
|
10
|
+
|
11
|
+
def initialize(session)
|
12
|
+
@session = session
|
13
|
+
super()
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.included(base)
|
18
|
+
base.extend ClassMethods
|
19
|
+
|
20
|
+
# I have to do this in a block to deal with some of Thor's DSL
|
21
|
+
base.class_eval do
|
22
|
+
extend Forwardable
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
no_commands do
|
27
|
+
def_delegators :shell, :session
|
28
|
+
def_delegators :session, :stdout, :stdin, :stderr, :browser
|
29
|
+
def_delegators :stdout, :puts, :print
|
30
|
+
def_delegators :stdin, :gets
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
def start(given_args = ARGV, config = {})
|
37
|
+
session = config.delete(:session)
|
38
|
+
config[:shell] = Shell.new(session) if session
|
39
|
+
super(given_args, config)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|