nightona 0.191.0

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE +190 -0
  7. data/README.md +184 -0
  8. data/Rakefile +12 -0
  9. data/lib/nightona/code_interpreter.rb +359 -0
  10. data/lib/nightona/common/charts.rb +124 -0
  11. data/lib/nightona/common/code_interpreter.rb +56 -0
  12. data/lib/nightona/common/code_language.rb +14 -0
  13. data/lib/nightona/common/file_system.rb +26 -0
  14. data/lib/nightona/common/git.rb +19 -0
  15. data/lib/nightona/common/image.rb +500 -0
  16. data/lib/nightona/common/nightona.rb +230 -0
  17. data/lib/nightona/common/process.rb +149 -0
  18. data/lib/nightona/common/pty.rb +309 -0
  19. data/lib/nightona/common/resources.rb +39 -0
  20. data/lib/nightona/common/response.rb +83 -0
  21. data/lib/nightona/common/snapshot.rb +124 -0
  22. data/lib/nightona/computer_use.rb +919 -0
  23. data/lib/nightona/config.rb +116 -0
  24. data/lib/nightona/file_system.rb +451 -0
  25. data/lib/nightona/file_transfer.rb +383 -0
  26. data/lib/nightona/git.rb +334 -0
  27. data/lib/nightona/lsp_server.rb +139 -0
  28. data/lib/nightona/nightona.rb +336 -0
  29. data/lib/nightona/object_storage.rb +172 -0
  30. data/lib/nightona/otel.rb +183 -0
  31. data/lib/nightona/process.rb +550 -0
  32. data/lib/nightona/sandbox.rb +751 -0
  33. data/lib/nightona/sdk/version.rb +10 -0
  34. data/lib/nightona/sdk.rb +56 -0
  35. data/lib/nightona/snapshot_service.rb +238 -0
  36. data/lib/nightona/util.rb +80 -0
  37. data/lib/nightona/volume.rb +46 -0
  38. data/lib/nightona/volume_service.rb +61 -0
  39. data/lib/nightona.rb +10 -0
  40. data/project.json +100 -0
  41. data/scripts/generate-docs.rb +402 -0
  42. data/sig/nightona/sdk.rbs +6 -0
  43. metadata +242 -0
@@ -0,0 +1,230 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # frozen_string_literal: true
5
+
6
+ module Nightona
7
+ CODE_TOOLBOX_LANGUAGE_LABEL = 'code-toolbox-language'
8
+
9
+ class CreateSandboxBaseParams
10
+ # @return [Symbol, nil] Programming language for the Sandbox
11
+ attr_accessor :language
12
+
13
+ # @return [String, nil] OS user for the Sandbox
14
+ attr_accessor :os_user
15
+
16
+ # @return [Hash<String, String>, nil] Environment variables to set in the Sandbox
17
+ attr_accessor :env_vars
18
+
19
+ # @return [Hash<String, String>, nil] Custom labels for the Sandbox
20
+ attr_accessor :labels
21
+
22
+ # @return [Boolean, nil] Whether the Sandbox should be public
23
+ attr_accessor :public
24
+
25
+ # @return [Float, nil] Timeout in seconds for Sandbox to be created and started
26
+ attr_accessor :timeout
27
+
28
+ # @return [Integer, nil] Auto-stop interval in minutes
29
+ attr_accessor :auto_stop_interval
30
+
31
+ # @return [Integer, nil] Auto-archive interval in minutes
32
+ attr_accessor :auto_archive_interval
33
+
34
+ # @return [Integer, nil] Auto-delete interval in minutes
35
+ attr_accessor :auto_delete_interval
36
+
37
+ # @return [Array<NightonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
38
+ attr_accessor :volumes
39
+
40
+ # @return [Boolean, nil] Whether to block all network access for the Sandbox
41
+ attr_accessor :network_block_all
42
+
43
+ # @return [String, nil] Comma-separated list of allowed CIDR network addresses for the Sandbox
44
+ attr_accessor :network_allow_list
45
+
46
+ # @return [String, nil] Comma-separated list of allowed domains for the Sandbox
47
+ attr_accessor :domain_allow_list
48
+
49
+ # @return [Boolean, nil] Whether the Sandbox should be ephemeral
50
+ attr_accessor :ephemeral
51
+
52
+ # @return [String, nil] ID or name of an existing Sandbox to link the new Sandbox to. The new
53
+ # Sandbox will be scheduled on the same runner as the linked Sandbox so a local network can be
54
+ # established between them. Linked Sandboxes must be
55
+ # ephemeral (auto_delete_interval=0) and cannot themselves be linked to another Sandbox.
56
+ attr_accessor :linked_sandbox
57
+
58
+ # Initialize CreateSandboxBaseParams
59
+ #
60
+ # @param language [Symbol, nil] Programming language for the Sandbox
61
+ # @param os_user [String, nil] OS user for the Sandbox
62
+ # @param env_vars [Hash<String, String>, nil] Environment variables to set in the Sandbox
63
+ # @param labels [Hash<String, String>, nil] Custom labels for the Sandbox
64
+ # @param public [Boolean, nil] Whether the Sandbox should be public
65
+ # @param timeout [Float, nil] Timeout in seconds for Sandbox to be created and started
66
+ # @param auto_stop_interval [Integer, nil] Auto-stop interval in minutes
67
+ # @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
68
+ # @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
69
+ # @param volumes [Array<NightonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
70
+ # @param network_block_all [Boolean, nil] Whether to block all network access for the Sandbox
71
+ # @param network_allow_list [String, nil] Comma-separated list of allowed CIDR network addresses for the Sandbox
72
+ # @param domain_allow_list [String, nil] Comma-separated list of allowed domains for the Sandbox
73
+ # @param ephemeral [Boolean, nil] Whether the Sandbox should be ephemeral
74
+ # @param linked_sandbox [String, nil] ID or name of an existing Sandbox to link the new Sandbox to
75
+ def initialize( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
76
+ language: nil,
77
+ os_user: nil,
78
+ env_vars: nil,
79
+ labels: nil,
80
+ public: nil,
81
+ timeout: nil,
82
+ auto_stop_interval: nil,
83
+ auto_archive_interval: nil,
84
+ auto_delete_interval: nil,
85
+ volumes: nil,
86
+ network_block_all: nil,
87
+ network_allow_list: nil,
88
+ domain_allow_list: nil,
89
+ ephemeral: nil,
90
+ linked_sandbox: nil
91
+ )
92
+ @language = language
93
+ @os_user = os_user
94
+ @env_vars = env_vars
95
+ @labels = labels
96
+ @public = public
97
+ @timeout = timeout
98
+ @auto_stop_interval = auto_stop_interval
99
+ @auto_archive_interval = auto_archive_interval
100
+ @auto_delete_interval = auto_delete_interval
101
+ @volumes = volumes
102
+ @network_block_all = network_block_all
103
+ @network_allow_list = network_allow_list
104
+ @domain_allow_list = domain_allow_list
105
+ @ephemeral = ephemeral
106
+ @linked_sandbox = linked_sandbox
107
+
108
+ # Handle ephemeral and auto_delete_interval conflict
109
+ handle_ephemeral_auto_delete_conflict
110
+ end
111
+
112
+ # Convert to hash representation
113
+ #
114
+ # @return [Hash<Symbol, Object>] Hash representation of the parameters
115
+ def to_h # rubocop:disable Metrics/MethodLength
116
+ {
117
+ language:,
118
+ os_user:,
119
+ env_vars:,
120
+ labels:,
121
+ public:,
122
+ timeout:,
123
+ auto_stop_interval:,
124
+ auto_archive_interval:,
125
+ auto_delete_interval:,
126
+ volumes:,
127
+ network_block_all:,
128
+ network_allow_list:,
129
+ domain_allow_list:,
130
+ ephemeral:,
131
+ linked_sandbox:
132
+ }.compact
133
+ end
134
+
135
+ private
136
+
137
+ # Handle the conflict between ephemeral and auto_delete_interval
138
+ #
139
+ # @return [void]
140
+ def handle_ephemeral_auto_delete_conflict
141
+ return unless ephemeral && auto_delete_interval && !auto_delete_interval.zero?
142
+
143
+ warn(
144
+ "'ephemeral' and 'auto_delete_interval' cannot be used together. " \
145
+ 'If ephemeral is true, auto_delete_interval will be ignored and set to 0.'
146
+ )
147
+ @auto_delete_interval = 0
148
+ end
149
+ end
150
+
151
+ class CreateSandboxFromImageParams < CreateSandboxBaseParams
152
+ # @return [String, Image] Custom Docker image to use for the Sandbox. If an Image object is provided,
153
+ # the image will be dynamically built.
154
+ attr_accessor :image
155
+
156
+ # @return [Nightona::Resources, nil] Resource configuration for the Sandbox. If not provided, sandbox will
157
+ # have default resources.
158
+ attr_accessor :resources
159
+
160
+ # Initialize CreateSandboxFromImageParams
161
+ #
162
+ # @param image [String, Image] Custom Docker image to use for the Sandbox
163
+ # @param resources [Nightona::Resources, nil] Resource configuration for the Sandbox
164
+ # @param language [Symbol, nil] Programming language for the Sandbox
165
+ # @param os_user [String, nil] OS user for the Sandbox
166
+ # @param env_vars [Hash<String, String>, nil] Environment variables to set in the Sandbox
167
+ # @param labels [Hash<String, String>, nil] Custom labels for the Sandbox
168
+ # @param public [Boolean, nil] Whether the Sandbox should be public
169
+ # @param timeout [Float, nil] Timeout in seconds for Sandbox to be created and started
170
+ # @param auto_stop_interval [Integer, nil] Auto-stop interval in minutes
171
+ # @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
172
+ # @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
173
+ # @param volumes [Array<NightonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
174
+ # @param network_block_all [Boolean, nil] Whether to block all network access for the Sandbox
175
+ # @param network_allow_list [String, nil] Comma-separated list of allowed CIDR network addresses for the Sandbox
176
+ # @param domain_allow_list [String, nil] Comma-separated list of allowed domains for the Sandbox
177
+ # @param ephemeral [Boolean, nil] Whether the Sandbox should be ephemeral
178
+ def initialize(image:, resources: nil, **args)
179
+ @image = image
180
+ @resources = resources
181
+
182
+ super(**args)
183
+ end
184
+
185
+ # Convert to hash representation
186
+ #
187
+ # @return [Hash<Symbol, Object>] Hash representation of the parameters
188
+ def to_h
189
+ super.merge(
190
+ image:,
191
+ resources: resources&.to_h
192
+ ).compact
193
+ end
194
+ end
195
+
196
+ class CreateSandboxFromSnapshotParams < CreateSandboxBaseParams
197
+ # @return [String, nil] Name of the snapshot to use for the Sandbox
198
+ attr_accessor :snapshot
199
+
200
+ # Initialize CreateSandboxFromSnapshotParams
201
+ #
202
+ # @param snapshot [String, nil] Name of the snapshot to use for the Sandbox
203
+ # @param language [Symbol, nil] Programming language for the Sandbox
204
+ # @param os_user [String, nil] OS user for the Sandbox
205
+ # @param env_vars [Hash<String, String>, nil] Environment variables to set in the Sandbox
206
+ # @param labels [Hash<String, String>, nil] Custom labels for the Sandbox
207
+ # @param public [Boolean, nil] Whether the Sandbox should be public
208
+ # @param timeout [Float, nil] Timeout in seconds for Sandbox to be created and started
209
+ # @param auto_stop_interval [Integer, nil] Auto-stop interval in minutes
210
+ # @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
211
+ # @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
212
+ # @param volumes [Array<NightonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
213
+ # @param network_block_all [Boolean, nil] Whether to block all network access for the Sandbox
214
+ # @param network_allow_list [String, nil] Comma-separated list of allowed CIDR network addresses for the Sandbox
215
+ # @param domain_allow_list [String, nil] Comma-separated list of allowed domains for the Sandbox
216
+ # @param ephemeral [Boolean, nil] Whether the Sandbox should be ephemeral
217
+ def initialize(snapshot: nil, **args)
218
+ @snapshot = snapshot
219
+
220
+ super(**args)
221
+ end
222
+
223
+ # Convert to hash representation
224
+ #
225
+ # @return [Hash<Symbol, Object>] Hash representation of the parameters
226
+ def to_h
227
+ super.merge(snapshot:).compact
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,149 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # frozen_string_literal: true
5
+
6
+ module Nightona
7
+ class ExecuteResponse
8
+ # @return [Integer] The exit code from the command execution
9
+ attr_reader :exit_code
10
+
11
+ # @return [String] The output from the command execution
12
+ attr_reader :result
13
+
14
+ # @return [ExecutionArtifacts, nil] Artifacts from the command execution
15
+ attr_reader :artifacts
16
+
17
+ # @return [Hash] Additional properties from the response
18
+ attr_reader :additional_properties
19
+
20
+ # Initialize a new ExecuteResponse
21
+ #
22
+ # @param exit_code [Integer] The exit code from the command execution
23
+ # @param result [String] The output from the command execution
24
+ # @param artifacts [ExecutionArtifacts, nil] Artifacts from the command execution
25
+ # @param additional_properties [Hash] Additional properties from the response
26
+ def initialize(exit_code:, result:, artifacts: nil, additional_properties: {})
27
+ @exit_code = exit_code
28
+ @result = result
29
+ @artifacts = artifacts
30
+ @additional_properties = additional_properties
31
+ end
32
+ end
33
+
34
+ class ExecutionArtifacts
35
+ # @return [String] Standard output from the command, same as `result` in `ExecuteResponse`
36
+ attr_accessor :stdout
37
+
38
+ # @return [Array] List of chart metadata from matplotlib
39
+ attr_accessor :charts
40
+
41
+ # Initialize a new ExecutionArtifacts
42
+ #
43
+ # @param stdout [String] Standard output from the command
44
+ # @param charts [Array] List of chart metadata from matplotlib
45
+ def initialize(stdout = '', charts = [])
46
+ @stdout = stdout
47
+ @charts = charts
48
+ end
49
+ end
50
+
51
+ class CodeRunParams
52
+ # @return [Array<String>, nil] Command line arguments
53
+ attr_accessor :argv
54
+
55
+ # @return [Hash<String, String>, nil] Environment variables
56
+ attr_accessor :env
57
+
58
+ # Initialize a new CodeRunParams
59
+ #
60
+ # @param argv [Array<String>, nil] Command line arguments
61
+ # @param env [Hash<String, String>, nil] Environment variables
62
+ def initialize(argv: nil, env: nil)
63
+ @argv = argv
64
+ @env = env
65
+ end
66
+ end
67
+
68
+ class SessionExecuteRequest
69
+ # @return [String] The command to execute
70
+ attr_accessor :command
71
+
72
+ # @return [Boolean] Whether to execute the command asynchronously
73
+ attr_accessor :run_async
74
+
75
+ # @return [Boolean] Whether to suppress input echo
76
+ attr_accessor :suppress_input_echo
77
+
78
+ # Initialize a new SessionExecuteRequest
79
+ #
80
+ # @param command [String] The command to execute
81
+ # @param run_async [Boolean] Whether to execute the command asynchronously
82
+ # @param suppress_input_echo [Boolean] Whether to suppress input echo (default is false)
83
+ def initialize(command:, run_async: false, suppress_input_echo: false)
84
+ @command = command
85
+ @run_async = run_async
86
+ @suppress_input_echo = suppress_input_echo
87
+ end
88
+ end
89
+
90
+ class SessionExecuteResponse
91
+ # @return [String, nil] Unique identifier for the executed command
92
+ attr_reader :cmd_id
93
+
94
+ # @return [String, nil] The output from the command execution
95
+ attr_reader :output
96
+
97
+ # @return [String, nil] Standard output from the command
98
+ attr_reader :stdout
99
+
100
+ # @return [String, nil] Standard error from the command
101
+ attr_reader :stderr
102
+
103
+ # @return [Integer, nil] The exit code from the command execution
104
+ attr_reader :exit_code
105
+
106
+ # @return [Hash] Additional properties from the response
107
+ attr_reader :additional_properties
108
+
109
+ # Initialize a new SessionExecuteResponse
110
+ #
111
+ # @param opts [Hash] Options for the SessionExecuteResponse
112
+ # @param cmd_id [String, nil] Unique identifier for the executed command
113
+ # @param output [String, nil] The output from the command execution
114
+ # @param stdout [String, nil] Standard output from the command
115
+ # @param stderr [String, nil] Standard error from the command
116
+ # @param exit_code [Integer, nil] The exit code from the command execution
117
+ # @param additional_properties [Hash] Additional properties from the response
118
+ def initialize(opts = {})
119
+ @cmd_id = opts.fetch(:cmd_id, nil)
120
+ @output = opts.fetch(:output, nil)
121
+ @stdout = opts.fetch(:stdout, nil)
122
+ @stderr = opts.fetch(:stderr, nil)
123
+ @exit_code = opts.fetch(:exit_code, nil)
124
+ @additional_properties = opts.fetch(:additional_properties, {})
125
+ end
126
+ end
127
+
128
+ class SessionCommandLogsResponse
129
+ # @return [String, nil] The combined output from the command
130
+ attr_reader :output
131
+
132
+ # @return [String, nil] The stdout from the command
133
+ attr_reader :stdout
134
+
135
+ # @return [String, nil] The stderr from the command
136
+ attr_reader :stderr
137
+
138
+ # Initialize a new SessionCommandLogsResponse
139
+ #
140
+ # @param output [String, nil] The combined output from the command
141
+ # @param stdout [String, nil] The stdout from the command
142
+ # @param stderr [String, nil] The stderr from the command
143
+ def initialize(output: nil, stdout: nil, stderr: nil)
144
+ @output = output
145
+ @stdout = stdout
146
+ @stderr = stderr
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,309 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # frozen_string_literal: true
5
+
6
+ require 'json'
7
+ require 'observer'
8
+
9
+ module Nightona
10
+ class PtySize
11
+ # @return [Integer] Number of terminal rows (height)
12
+ attr_reader :rows
13
+
14
+ # @return [Integer] Number of terminal columns (width)
15
+ attr_reader :cols
16
+
17
+ # Initialize a new PtySize
18
+ #
19
+ # @param rows [Integer] Number of terminal rows (height)
20
+ # @param cols [Integer] Number of terminal columns (width)
21
+ def initialize(rows:, cols:)
22
+ @rows = rows
23
+ @cols = cols
24
+ end
25
+ end
26
+
27
+ class PtyResult
28
+ # @return [Integer, nil] Exit code of the PTY process (0 for success, non-zero for errors).
29
+ # nil if the process hasn't exited yet or exit code couldn't be determined.
30
+ attr_reader :exit_code
31
+
32
+ # @return [String, nil] Error message if the PTY failed or was terminated abnormally.
33
+ # nil if no error occurred.
34
+ attr_reader :error
35
+
36
+ # Initialize a new PtyResult
37
+ #
38
+ # @param exit_code [Integer, nil] Exit code of the PTY process
39
+ # @param error [String, nil] Error message if the PTY failed
40
+ def initialize(exit_code: nil, error: nil)
41
+ @exit_code = exit_code
42
+ @error = error
43
+ end
44
+ end
45
+
46
+ class PtyHandle # rubocop:disable Metrics/ClassLength
47
+ include Observable
48
+
49
+ # @return [String] Session ID of the PTY session
50
+ attr_reader :session_id
51
+
52
+ # @return [Integer, nil] Exit code of the PTY process (if terminated)
53
+ attr_reader :exit_code
54
+
55
+ # @return [String, nil] Error message if the PTY failed
56
+ attr_reader :error
57
+
58
+ # Initialize the PTY handle.
59
+ #
60
+ # @param websocket [WebSocket::Client::Simple::Client] Connected WebSocket client connection
61
+ # @param session_id [String] Session ID of the PTY session
62
+ # @param handle_resize [Proc, nil] Optional callback for resizing the PTY
63
+ # @param handle_kill [Proc, nil] Optional callback for killing the PTY
64
+ def initialize(websocket, session_id:, handle_resize: nil, handle_kill: nil)
65
+ @websocket = websocket
66
+ @session_id = session_id
67
+ @handle_resize = handle_resize
68
+ @handle_kill = handle_kill
69
+ @exit_code = nil
70
+ @error = nil
71
+ @logger = Sdk.logger
72
+
73
+ @status = Status::INIT
74
+ subscribe
75
+ end
76
+
77
+ # Check if connected to the PTY session
78
+ #
79
+ # @return [Boolean] true if connected, false otherwise
80
+ def connected? = websocket.open?
81
+
82
+ # Wait for the PTY connection to be established
83
+ #
84
+ # @param timeout [Float] Maximum time in seconds to wait for connection. Defaults to 10.0
85
+ # @return [void]
86
+ # @raise [Nightona::Sdk::Error] If connection timeout is exceeded
87
+ def wait_for_connection(timeout: DEFAULT_TIMEOUT)
88
+ return if status == Status::CONNECTED
89
+
90
+ start_time = Time.now
91
+
92
+ sleep(SLEEP_INTERVAL) until status == Status::CONNECTED || (Time.now - start_time) > timeout
93
+
94
+ raise Sdk::Error, 'PTY connection timeout' unless status == Status::CONNECTED
95
+ end
96
+
97
+ # Send input to the PTY session
98
+ #
99
+ # @param input [String] Input to send to the PTY
100
+ # @return [void]
101
+ def send_input(input)
102
+ raise Sdk::Error, 'PTY session not connected' unless websocket.open?
103
+
104
+ websocket.send(input)
105
+ end
106
+
107
+ # Resize the PTY terminal
108
+ #
109
+ # @param pty_size [PtySize] New terminal size
110
+ # @return [NightonaApiClient::PtySessionInfo] Updated PTY session information
111
+ def resize(pty_size)
112
+ raise Sdk::Error, 'No resize handler available' unless handle_resize
113
+
114
+ handle_resize.call(pty_size)
115
+ end
116
+
117
+ # Delete the PTY session
118
+ #
119
+ # @return [void]
120
+ def kill
121
+ raise Sdk::Error, 'No kill handler available' unless handle_kill
122
+
123
+ handle_kill.call
124
+ end
125
+
126
+ # Wait for the PTY session to complete
127
+ #
128
+ # @param on_data [Proc, nil] Optional callback to handle output data
129
+ # @return [Nightona::PtyResult] Result containing exit code and error information
130
+ def wait(timeout: nil, &on_data)
131
+ timeout ||= Float::INFINITY
132
+ return unless status == Status::CONNECTED
133
+
134
+ start_time = Time.now
135
+ add_observer(on_data, :call) if on_data
136
+
137
+ sleep(SLEEP_INTERVAL) while status == Status::CONNECTED && (Time.now - start_time) <= timeout
138
+
139
+ PtyResult.new(exit_code:, error:)
140
+ ensure
141
+ delete_observer(on_data) if on_data
142
+ end
143
+
144
+ # @yieldparam [WebSocket::Frame::Data]
145
+ # @return [void]
146
+ def each(&)
147
+ return unless block_given?
148
+
149
+ queue = Queue.new
150
+ add_observer(proc { queue << _1 }, :call)
151
+
152
+ while websocket.open?
153
+ drain(queue, &)
154
+ sleep(SLEEP_INTERVAL)
155
+ end
156
+
157
+ drain(queue, &)
158
+ end
159
+
160
+ # Disconnect from the PTY session
161
+ #
162
+ # @return [void]
163
+ def disconnect = websocket.close
164
+
165
+ private
166
+
167
+ # @return [Symbol]
168
+ attr_reader :status
169
+
170
+ # @return [WebSocket::Client::Simple::Client]
171
+ attr_reader :websocket
172
+
173
+ # @return [Proc, Nil]
174
+ attr_reader :handle_kill
175
+
176
+ # @return [Proc, Nil]
177
+ attr_reader :handle_resize
178
+
179
+ # @return [Logger]
180
+ attr_reader :logger
181
+
182
+ # @return [void]
183
+ def subscribe
184
+ websocket.on(:open, &method(:on_websocket_open))
185
+ websocket.on(:close, &method(:on_websocket_close))
186
+ websocket.on(:message, &method(:on_websocket_message))
187
+ websocket.on(:error, &method(:on_websocket_error))
188
+ end
189
+
190
+ # @return [void]
191
+ def on_websocket_open
192
+ logger.debug('[Websocket] open')
193
+ @status = Status::OPEN
194
+ end
195
+
196
+ # @param error [Object, Nil]
197
+ # @return [void]
198
+ def on_websocket_close(error)
199
+ logger.debug("[Websocket] close: #{error.inspect}")
200
+ @status = Status::CLOSED
201
+ end
202
+
203
+ # @param error [WebSocket::Frame::Incoming::Client]
204
+ # @return [void]
205
+ def on_websocket_message(message)
206
+ logger.debug("[Websocket] message(#{message.type}): #{message.data}")
207
+
208
+ case message.type
209
+ when :binary, :text
210
+ process_websocket_text_message(message)
211
+ when :close
212
+ process_websocket_close_message(message)
213
+ end
214
+ end
215
+
216
+ # @param error [Object]
217
+ # @return [void]
218
+ def on_websocket_error(error)
219
+ logger.debug("[Websocket] error: #{error.inspect}")
220
+ logger.debug("[Websocket] error: #{error.class}")
221
+ @status = Status::ERROR
222
+ end
223
+
224
+ # @param message [WebSocket::Frame::Incoming::Client]
225
+ # @return [void]
226
+ def process_websocket_text_message(message)
227
+ data = JSON.parse(message.data.to_s, symbolize_names: true)
228
+ process_websocket_control_message(data) if data[:type] == WebSocketMessageType::CONTROL
229
+ rescue JSON::ParserError, TypeError
230
+ process_websocket_data_message(message.data.to_s)
231
+ end
232
+
233
+ # @param data [WebSocket::Frame::Data]
234
+ # @return [void]
235
+ def process_websocket_data_message(data)
236
+ changed
237
+ notify_observers(data)
238
+ end
239
+
240
+ # @param data [WebSocket::Frame::Data]
241
+ # @return [void]
242
+ def process_websocket_control_message(data) # rubocop:disable Metrics/MethodLength
243
+ case data[:status]
244
+ when WebSocketControlStatus::CONNECTED
245
+ logger.debug('[control] connected')
246
+ @status = Status::CONNECTED
247
+ when WebSocketControlStatus::ERROR
248
+ logger.debug("[control] error: #{error.inspect}")
249
+ @status = Status::ERROR
250
+ @error = data.fetch(:error, 'Unknown connection error')
251
+ else
252
+ websocket.close
253
+ raise Sdk::Error, "Received invalid control message status: #{data[:status]}"
254
+ end
255
+ end
256
+
257
+ # @param message [WebSocket::Frame::Incoming::Client]
258
+ # @return [void]
259
+ def process_websocket_close_message(message)
260
+ data = JSON.parse(message.data.to_s, symbolize_names: true)
261
+ @exit_code = data.fetch(:exitCode, nil)
262
+ @error = data.fetch(:exitReason, nil)
263
+
264
+ disconnect
265
+ rescue JSON::ParserError, TypeError
266
+ nil
267
+ end
268
+
269
+ # @param queue [Queue]
270
+ # @yieldparam [WebSocket::Frame::Data]
271
+ # @return [void]
272
+ def drain(queue)
273
+ data = nil
274
+
275
+ yield data while (data = queue.pop(true))
276
+ rescue ThreadError => _e
277
+ nil
278
+ end
279
+
280
+ DEFAULT_TIMEOUT = 10.0
281
+ private_constant :DEFAULT_TIMEOUT
282
+
283
+ SLEEP_INTERVAL = 0.1
284
+ private_constant :SLEEP_INTERVAL
285
+
286
+ module Status
287
+ ALL = [
288
+ INIT = 'init',
289
+ OPEN = 'open',
290
+ CONNECTED = 'connected',
291
+ CLOSED = 'closed',
292
+ ERROR = 'error'
293
+ ].freeze
294
+ end
295
+
296
+ module WebSocketMessageType
297
+ ALL = [
298
+ CONTROL = 'control'
299
+ ].freeze
300
+ end
301
+
302
+ module WebSocketControlStatus
303
+ ALL = [
304
+ CONNECTED = 'connected',
305
+ ERROR = 'error'
306
+ ].freeze
307
+ end
308
+ end
309
+ end