daytona-sdk 0.125.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.
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'observer'
5
+
6
+ module Daytona
7
+ class PtySize
8
+ # @return [Integer] Number of terminal rows (height)
9
+ attr_reader :rows
10
+
11
+ # @return [Integer] Number of terminal columns (width)
12
+ attr_reader :cols
13
+
14
+ # Initialize a new PtySize
15
+ #
16
+ # @param rows [Integer] Number of terminal rows (height)
17
+ # @param cols [Integer] Number of terminal columns (width)
18
+ def initialize(rows:, cols:)
19
+ @rows = rows
20
+ @cols = cols
21
+ end
22
+ end
23
+
24
+ class PtyResult
25
+ # @return [Integer, nil] Exit code of the PTY process (0 for success, non-zero for errors).
26
+ # nil if the process hasn't exited yet or exit code couldn't be determined.
27
+ attr_reader :exit_code
28
+
29
+ # @return [String, nil] Error message if the PTY failed or was terminated abnormally.
30
+ # nil if no error occurred.
31
+ attr_reader :error
32
+
33
+ # Initialize a new PtyResult
34
+ #
35
+ # @param exit_code [Integer, nil] Exit code of the PTY process
36
+ # @param error [String, nil] Error message if the PTY failed
37
+ def initialize(exit_code: nil, error: nil)
38
+ @exit_code = exit_code
39
+ @error = error
40
+ end
41
+ end
42
+
43
+ class PtyHandle # rubocop:disable Metrics/ClassLength
44
+ include Observable
45
+
46
+ # @return [String] Session ID of the PTY session
47
+ attr_reader :session_id
48
+
49
+ # @return [Integer, nil] Exit code of the PTY process (if terminated)
50
+ attr_reader :exit_code
51
+
52
+ # @return [String, nil] Error message if the PTY failed
53
+ attr_reader :error
54
+
55
+ # Initialize the PTY handle.
56
+ #
57
+ # @param websocket [WebSocket::Client::Simple::Client] Connected WebSocket client connection
58
+ # @param session_id [String] Session ID of the PTY session
59
+ # @param handle_resize [Proc, nil] Optional callback for resizing the PTY
60
+ # @param handle_kill [Proc, nil] Optional callback for killing the PTY
61
+ def initialize(websocket, session_id:, handle_resize: nil, handle_kill: nil)
62
+ @websocket = websocket
63
+ @session_id = session_id
64
+ @handle_resize = handle_resize
65
+ @handle_kill = handle_kill
66
+ @exit_code = nil
67
+ @error = nil
68
+ @logger = Sdk.logger
69
+
70
+ @status = Status::INIT
71
+ subscribe
72
+ end
73
+
74
+ # Check if connected to the PTY session
75
+ #
76
+ # @return [Boolean] true if connected, false otherwise
77
+ def connected? = websocket.open?
78
+
79
+ # Wait for the PTY connection to be established
80
+ #
81
+ # @param timeout [Float] Maximum time in seconds to wait for connection. Defaults to 10.0
82
+ # @return [void]
83
+ # @raise [Daytona::Sdk::Error] If connection timeout is exceeded
84
+ def wait_for_connection(timeout: DEFAULT_TIMEOUT)
85
+ return if status == Status::CONNECTED
86
+
87
+ start_time = Time.now
88
+
89
+ sleep(SLEEP_INTERVAL) until status == Status::CONNECTED || (Time.now - start_time) > timeout
90
+
91
+ raise Sdk::Error, 'PTY connection timeout' unless status == Status::CONNECTED
92
+ end
93
+
94
+ # Send input to the PTY session
95
+ #
96
+ # @param input [String] Input to send to the PTY
97
+ # @return [void]
98
+ def send_input(input)
99
+ raise Sdk::Error, 'PTY session not connected' unless websocket.open?
100
+
101
+ websocket.send(input)
102
+ end
103
+
104
+ # Resize the PTY terminal
105
+ #
106
+ # @param pty_size [PtySize] New terminal size
107
+ # @return [DaytonaApiClient::PtySessionInfo] Updated PTY session information
108
+ def resize(pty_size)
109
+ raise Sdk::Error, 'No resize handler available' unless handle_resize
110
+
111
+ handle_resize.call(pty_size)
112
+ end
113
+
114
+ # Delete the PTY session
115
+ #
116
+ # @return [void]
117
+ def kill
118
+ raise Sdk::Error, 'No kill handler available' unless handle_kill
119
+
120
+ handle_kill.call
121
+ end
122
+
123
+ # Wait for the PTY session to complete
124
+ #
125
+ # @param on_data [Proc, nil] Optional callback to handle output data
126
+ # @return [Daytona::PtyResult] Result containing exit code and error information
127
+ def wait(timeout: nil, &on_data)
128
+ timeout ||= Float::INFINITY
129
+ return unless status == Status::CONNECTED
130
+
131
+ start_time = Time.now
132
+ add_observer(on_data, :call) if on_data
133
+
134
+ sleep(SLEEP_INTERVAL) while status == Status::CONNECTED && (Time.now - start_time) <= timeout
135
+
136
+ PtyResult.new(exit_code:, error:)
137
+ ensure
138
+ delete_observer(on_data) if on_data
139
+ end
140
+
141
+ # @yieldparam [WebSocket::Frame::Data]
142
+ # @return [void]
143
+ def each(&)
144
+ return unless block_given?
145
+
146
+ queue = Queue.new
147
+ add_observer(proc { queue << _1 }, :call)
148
+
149
+ while websocket.open?
150
+ drain(queue, &)
151
+ sleep(SLEEP_INTERVAL)
152
+ end
153
+
154
+ drain(queue, &)
155
+ end
156
+
157
+ # Disconnect from the PTY session
158
+ #
159
+ # @return [void]
160
+ def disconnect = websocket.close
161
+
162
+ private
163
+
164
+ # @return [Symbol]
165
+ attr_reader :status
166
+
167
+ # @return [WebSocket::Client::Simple::Client]
168
+ attr_reader :websocket
169
+
170
+ # @return [Proc, Nil]
171
+ attr_reader :handle_kill
172
+
173
+ # @return [Proc, Nil]
174
+ attr_reader :handle_resize
175
+
176
+ # @return [Logger]
177
+ attr_reader :logger
178
+
179
+ # @return [void]
180
+ def subscribe
181
+ websocket.on(:open, &method(:on_websocket_open))
182
+ websocket.on(:close, &method(:on_websocket_close))
183
+ websocket.on(:message, &method(:on_websocket_message))
184
+ websocket.on(:error, &method(:on_websocket_error))
185
+ end
186
+
187
+ # @return [void]
188
+ def on_websocket_open
189
+ logger.debug('[Websocket] open')
190
+ @status = Status::OPEN
191
+ end
192
+
193
+ # @param error [Object, Nil]
194
+ # @return [void]
195
+ def on_websocket_close(error)
196
+ logger.debug("[Websocket] close: #{error.inspect}")
197
+ @status = Status::CLOSED
198
+ end
199
+
200
+ # @param error [WebSocket::Frame::Incoming::Client]
201
+ # @return [void]
202
+ def on_websocket_message(message)
203
+ logger.debug("[Websocket] message(#{message.type}): #{message.data}")
204
+
205
+ case message.type
206
+ when :binary, :text
207
+ process_websocket_text_message(message)
208
+ when :close
209
+ process_websocket_close_message(message)
210
+ end
211
+ end
212
+
213
+ # @param error [Object]
214
+ # @return [void]
215
+ def on_websocket_error(error)
216
+ logger.debug("[Websocket] error: #{error.inspect}")
217
+ logger.debug("[Websocket] error: #{error.class}")
218
+ @status = Status::ERROR
219
+ end
220
+
221
+ # @param message [WebSocket::Frame::Incoming::Client]
222
+ # @return [void]
223
+ def process_websocket_text_message(message)
224
+ data = JSON.parse(message.data.to_s, symbolize_names: true)
225
+ process_websocket_control_message(data) if data[:type] == WebSocketMessageType::CONTROL
226
+ rescue JSON::ParserError, TypeError
227
+ process_websocket_data_message(message.data.to_s)
228
+ end
229
+
230
+ # @param data [WebSocket::Frame::Data]
231
+ # @return [void]
232
+ def process_websocket_data_message(data)
233
+ changed
234
+ notify_observers(data)
235
+ end
236
+
237
+ # @param data [WebSocket::Frame::Data]
238
+ # @return [void]
239
+ def process_websocket_control_message(data) # rubocop:disable Metrics/MethodLength
240
+ case data[:status]
241
+ when WebSocketControlStatus::CONNECTED
242
+ logger.debug('[control] connected')
243
+ @status = Status::CONNECTED
244
+ when WebSocketControlStatus::ERROR
245
+ logger.debug("[control] error: #{error.inspect}")
246
+ @status = Status::ERROR
247
+ @error = data.fetch(:error, 'Unknown connection error')
248
+ else
249
+ websocket.close
250
+ raise Sdk::Error, "Received invalid control message status: #{data[:status]}"
251
+ end
252
+ end
253
+
254
+ # @param message [WebSocket::Frame::Incoming::Client]
255
+ # @return [void]
256
+ def process_websocket_close_message(message)
257
+ data = JSON.parse(message.data.to_s, symbolize_names: true)
258
+ @exit_code = data.fetch(:exitCode, nil)
259
+ @error = data.fetch(:exitReason, nil)
260
+
261
+ disconnect
262
+ rescue JSON::ParserError, TypeError
263
+ nil
264
+ end
265
+
266
+ # @param queue [Queue]
267
+ # @yieldparam [WebSocket::Frame::Data]
268
+ # @return [void]
269
+ def drain(queue)
270
+ data = nil
271
+
272
+ yield data while (data = queue.pop(true))
273
+ rescue ThreadError => _e
274
+ nil
275
+ end
276
+
277
+ DEFAULT_TIMEOUT = 10.0
278
+ private_constant :DEFAULT_TIMEOUT
279
+
280
+ SLEEP_INTERVAL = 0.1
281
+ private_constant :SLEEP_INTERVAL
282
+
283
+ module Status
284
+ ALL = [
285
+ INIT = 'init',
286
+ OPEN = 'open',
287
+ CONNECTED = 'connected',
288
+ CLOSED = 'closed',
289
+ ERROR = 'error'
290
+ ].freeze
291
+ end
292
+
293
+ module WebSocketMessageType
294
+ ALL = [
295
+ CONTROL = 'control'
296
+ ].freeze
297
+ end
298
+
299
+ module WebSocketControlStatus
300
+ ALL = [
301
+ CONNECTED = 'connected',
302
+ ERORR = 'error'
303
+ ].freeze
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Daytona
4
+ class Resources
5
+ # @return [Integer, nil] Number of CPU cores to allocate
6
+ attr_reader :cpu
7
+
8
+ # @return [Integer, nil] Amount of memory in GiB to allocate
9
+ attr_reader :memory
10
+
11
+ # @return [Integer, nil] Amount of disk space in GiB to allocate
12
+ attr_reader :disk
13
+
14
+ # @return [Integer, nil] Number of GPUs to allocate
15
+ attr_reader :gpu
16
+
17
+ # @param cpu [Integer, nil] Number of CPU cores to allocate
18
+ # @param memory [Integer, nil] Amount of memory in GiB to allocate
19
+ # @param disk [Integer, nil] Amount of disk space in GiB to allocate
20
+ # @param gpu [Integer, nil] Number of GPUs to allocate
21
+ def initialize(cpu: nil, memory: nil, disk: nil, gpu: nil)
22
+ @cpu = cpu
23
+ @memory = memory
24
+ @disk = disk
25
+ @gpu = gpu
26
+ end
27
+
28
+ # @return [Hash] Hash representation of the resources
29
+ def to_h = { cpu:, memory:, disk:, gpu: }.compact
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Daytona
4
+ class PaginatedResource
5
+ # @return [Array<Object>]
6
+ attr_reader :items
7
+
8
+ # @return [Float]
9
+ attr_reader :page
10
+
11
+ # @return [Float]
12
+ attr_reader :total
13
+
14
+ # @return [Float]
15
+ attr_reader :total_pages
16
+
17
+ # @param items [Daytona::Sandbox]
18
+ # @param page [Float]
19
+ # @param total [Float]
20
+ # @param total_pages [Float]
21
+ def initialize(items:, page:, total:, total_pages:)
22
+ @items = items
23
+ @page = page
24
+ @total = total
25
+ @total_pages = total_pages
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Daytona
4
+ class CreateSnapshotParams
5
+ # @return [String] Name of the snapshot
6
+ attr_reader :name
7
+
8
+ # @return [String, Daytona::Image] Image of the snapshot. If a string is provided,
9
+ # it should be available on some registry. If an Image instance is provided,
10
+ # it will be used to create a new image in Daytona.
11
+ attr_reader :image
12
+
13
+ # @return [Daytona::Resources, nil] Resources of the snapshot
14
+ attr_reader :resources
15
+
16
+ # @return [Array<String>, nil] Entrypoint of the snapshot
17
+ attr_reader :entrypoint
18
+
19
+ # @param name [String] Name of the snapshot
20
+ # @param image [String, Daytona::Image] Image of the snapshot
21
+ # @param resources [Daytona::Resources, nil] Resources of the snapshot
22
+ # @param entrypoint [Array<String>, nil] Entrypoint of the snapshot
23
+ def initialize(name:, image:, resources: nil, entrypoint: nil)
24
+ @name = name
25
+ @image = image
26
+ @resources = resources
27
+ @entrypoint = entrypoint
28
+ end
29
+ end
30
+
31
+ class Snapshot
32
+ # @return [String] Unique identifier for the Snapshot
33
+ attr_reader :id
34
+
35
+ # @return [String, nil] Organization ID of the Snapshot
36
+ attr_reader :organization_id
37
+
38
+ # @return [Boolean, nil] Whether the Snapshot is general
39
+ attr_reader :general
40
+
41
+ # @return [String] Name of the Snapshot
42
+ attr_reader :name
43
+
44
+ # @return [String] Name of the Image of the Snapshot
45
+ attr_reader :image_name
46
+
47
+ # @return [String] State of the Snapshot
48
+ attr_reader :state
49
+
50
+ # @return [Float, nil] Size of the Snapshot
51
+ attr_reader :size
52
+
53
+ # @return [Array<String>, nil] Entrypoint of the Snapshot
54
+ attr_reader :entrypoint
55
+
56
+ # @return [Float] CPU of the Snapshot
57
+ attr_reader :cpu
58
+
59
+ # @return [Float] GPU of the Snapshot
60
+ attr_reader :gpu
61
+
62
+ # @return [Float] Memory of the Snapshot in GiB
63
+ attr_reader :mem
64
+
65
+ # @return [Float] Disk of the Snapshot in GiB
66
+ attr_reader :disk
67
+
68
+ # @return [String, nil] Error reason of the Snapshot
69
+ attr_reader :error_reason
70
+
71
+ # @return [String] Timestamp when the Snapshot was created
72
+ attr_reader :created_at
73
+
74
+ # @return [String] Timestamp when the Snapshot was last updated
75
+ attr_reader :updated_at
76
+
77
+ # @return [String, nil] Timestamp when the Snapshot was last used
78
+ attr_reader :last_used_at
79
+
80
+ # @return [DaytonaApiClient::BuildInfo, nil] Build information for the snapshot
81
+ attr_reader :build_info
82
+
83
+ # @param snapshot_dto [DaytonaApiClient::SnapshotDto] The snapshot DTO from the API
84
+ def initialize(snapshot_dto) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
85
+ @id = snapshot_dto.id
86
+ @organization_id = snapshot_dto.organization_id
87
+ @general = snapshot_dto.general
88
+ @name = snapshot_dto.name
89
+ @image_name = snapshot_dto.image_name
90
+ @state = snapshot_dto.state
91
+ @size = snapshot_dto.size
92
+ @entrypoint = snapshot_dto.entrypoint
93
+ @cpu = snapshot_dto.cpu
94
+ @gpu = snapshot_dto.gpu
95
+ @mem = snapshot_dto.mem
96
+ @disk = snapshot_dto.disk
97
+ @error_reason = snapshot_dto.error_reason
98
+ @created_at = snapshot_dto.created_at
99
+ @updated_at = snapshot_dto.updated_at
100
+ @last_used_at = snapshot_dto.last_used_at
101
+ @build_info = snapshot_dto.build_info
102
+ end
103
+
104
+ # Creates a Snapshot instance from a SnapshotDto
105
+ #
106
+ # @param dto [DaytonaApiClient::SnapshotDto] The snapshot DTO from the API
107
+ # @return [Daytona::Snapshot] The snapshot instance
108
+ def self.from_dto(dto) = new(dto)
109
+ end
110
+ end