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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +190 -0
- data/README.md +184 -0
- data/Rakefile +12 -0
- data/lib/nightona/code_interpreter.rb +359 -0
- data/lib/nightona/common/charts.rb +124 -0
- data/lib/nightona/common/code_interpreter.rb +56 -0
- data/lib/nightona/common/code_language.rb +14 -0
- data/lib/nightona/common/file_system.rb +26 -0
- data/lib/nightona/common/git.rb +19 -0
- data/lib/nightona/common/image.rb +500 -0
- data/lib/nightona/common/nightona.rb +230 -0
- data/lib/nightona/common/process.rb +149 -0
- data/lib/nightona/common/pty.rb +309 -0
- data/lib/nightona/common/resources.rb +39 -0
- data/lib/nightona/common/response.rb +83 -0
- data/lib/nightona/common/snapshot.rb +124 -0
- data/lib/nightona/computer_use.rb +919 -0
- data/lib/nightona/config.rb +116 -0
- data/lib/nightona/file_system.rb +451 -0
- data/lib/nightona/file_transfer.rb +383 -0
- data/lib/nightona/git.rb +334 -0
- data/lib/nightona/lsp_server.rb +139 -0
- data/lib/nightona/nightona.rb +336 -0
- data/lib/nightona/object_storage.rb +172 -0
- data/lib/nightona/otel.rb +183 -0
- data/lib/nightona/process.rb +550 -0
- data/lib/nightona/sandbox.rb +751 -0
- data/lib/nightona/sdk/version.rb +10 -0
- data/lib/nightona/sdk.rb +56 -0
- data/lib/nightona/snapshot_service.rb +238 -0
- data/lib/nightona/util.rb +80 -0
- data/lib/nightona/volume.rb +46 -0
- data/lib/nightona/volume_service.rb +61 -0
- data/lib/nightona.rb +10 -0
- data/project.json +100 -0
- data/scripts/generate-docs.rb +402 -0
- data/sig/nightona/sdk.rbs +6 -0
- 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
|