daytona 0.126.0.pre.alpha.4

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,480 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module Daytona
8
+ class Process # rubocop:disable Metrics/ClassLength
9
+ # @return [Daytona::SandboxPythonCodeToolbox,
10
+ attr_reader :code_toolbox
11
+
12
+ # @return [String] The ID of the Sandbox
13
+ attr_reader :sandbox_id
14
+
15
+ # @return [DaytonaToolboxApiClient::ProcessApi] API client for Sandbox operations
16
+ attr_reader :toolbox_api
17
+
18
+ # @return [Proc] Function to get preview link for a port
19
+ attr_reader :get_preview_link
20
+
21
+ # Initialize a new Process instance
22
+ #
23
+ # @param code_toolbox [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
24
+ # @param sandbox_id [String] The ID of the Sandbox
25
+ # @param toolbox_api [DaytonaToolboxApiClient::ProcessApi] API client for Sandbox operations
26
+ # @param get_preview_link [Proc] Function to get preview link for a port
27
+ def initialize(code_toolbox:, sandbox_id:, toolbox_api:, get_preview_link:)
28
+ @code_toolbox = code_toolbox
29
+ @sandbox_id = sandbox_id
30
+ @toolbox_api = toolbox_api
31
+ @get_preview_link = get_preview_link
32
+ end
33
+
34
+ # Execute a shell command in the Sandbox
35
+ #
36
+ # @param command [String] Shell command to execute
37
+ # @param cwd [String, nil] Working directory for command execution. If not specified, uses the sandbox working directory
38
+ # @param env [Hash<String, String>, nil] Environment variables to set for the command
39
+ # @param timeout [Integer, nil] Maximum time in seconds to wait for the command to complete. 0 means wait indefinitely
40
+ # @return [ExecuteResponse] Command execution results containing exit_code, result, and artifacts
41
+ #
42
+ # @example
43
+ # # Simple command
44
+ # response = sandbox.process.exec("echo 'Hello'")
45
+ # puts response.artifacts.stdout
46
+ # => "Hello\n"
47
+ #
48
+ # # Command with working directory
49
+ # result = sandbox.process.exec("ls", cwd: "workspace/src")
50
+ #
51
+ # # Command with timeout
52
+ # result = sandbox.process.exec("sleep 10", timeout: 5)
53
+ def exec(command:, cwd: nil, env: nil, timeout: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
54
+ command = "echo '#{Base64.encode64(command)}' | base64 -d | sh"
55
+
56
+ if env && !env.empty?
57
+ safe_env_exports = env.map do |key, value|
58
+ "export #{key}=$(echo '#{Base64.encode64(value)}' | base64 -d)"
59
+ end.join(';')
60
+ command = "#{safe_env_exports}; #{command}"
61
+ end
62
+
63
+ command = "sh -c \"#{command}\""
64
+
65
+ response = toolbox_api.execute_command(DaytonaToolboxApiClient::ExecuteRequest.new(command:, cwd:, timeout:))
66
+ # Post-process the output to extract ExecutionArtifacts
67
+ artifacts = parse_output(response.result.split("\n"))
68
+
69
+ # Create new response with processed output and charts
70
+ ExecuteResponse.new(
71
+ exit_code: response.exit_code,
72
+ result: artifacts.stdout,
73
+ artifacts: artifacts
74
+ )
75
+ end
76
+
77
+ # Execute code in the Sandbox using the appropriate language runtime
78
+ #
79
+ # @param code [String] Code to execute
80
+ # @param params [CodeRunParams, nil] Parameters for code execution
81
+ # @param timeout [Integer, nil] Maximum time in seconds to wait for the code to complete. 0 means wait indefinitely
82
+ # @return [ExecuteResponse] Code execution result containing exit_code, result, and artifacts
83
+ #
84
+ # @example
85
+ # # Run Python code
86
+ # response = sandbox.process.code_run(<<~CODE)
87
+ # x = 10
88
+ # y = 20
89
+ # print(f"Sum: {x + y}")
90
+ # CODE
91
+ # puts response.artifacts.stdout # Prints: Sum: 30
92
+ def code_run(code:, params: nil, timeout: nil)
93
+ exec(command: code_toolbox.get_run_command(code, params), env: params&.env, timeout:)
94
+ end
95
+
96
+ # Creates a new long-running background session in the Sandbox
97
+ #
98
+ # Sessions are background processes that maintain state between commands, making them ideal for
99
+ # scenarios requiring multiple related commands or persistent environment setup.
100
+ #
101
+ # @param session_id [String] Unique identifier for the new session
102
+ # @return [void]
103
+ #
104
+ # @example
105
+ # # Create a new session
106
+ # session_id = "my-session"
107
+ # sandbox.process.create_session(session_id)
108
+ # session = sandbox.process.get_session(session_id)
109
+ # # Do work...
110
+ # sandbox.process.delete_session(session_id)
111
+ def create_session(session_id)
112
+ toolbox_api.create_session(DaytonaToolboxApiClient::CreateSessionRequest.new(session_id:))
113
+ end
114
+
115
+ # Gets a session in the Sandbox
116
+ #
117
+ # @param session_id [String] Unique identifier of the session to retrieve
118
+ # @return [DaytonaApiClient::Session] Session information including session_id and commands
119
+ #
120
+ # @example
121
+ # session = sandbox.process.get_session("my-session")
122
+ # session.commands.each do |cmd|
123
+ # puts "Command: #{cmd.command}"
124
+ # end
125
+ def get_session(session_id) = toolbox_api.get_session(session_id)
126
+
127
+ # Gets information about a specific command executed in a session
128
+ #
129
+ # @param session_id [String] Unique identifier of the session
130
+ # @param command_id [String] Unique identifier of the command
131
+ # @return [DaytonaApiClient::Command] Command information including id, command, and exit_code
132
+ #
133
+ # @example
134
+ # cmd = sandbox.process.get_session_command(session_id: "my-session", command_id: "cmd-123")
135
+ # if cmd.exit_code == 0
136
+ # puts "Command #{cmd.command} completed successfully"
137
+ # end
138
+ def get_session_command(session_id:, command_id:)
139
+ toolbox_api.get_session_command(session_id, command_id)
140
+ end
141
+
142
+ # Executes a command in the session
143
+ #
144
+ # @param session_id [String] Unique identifier of the session to use
145
+ # @param req [Daytona::SessionExecuteRequest] Command execution request containing command and run_async
146
+ # @return [Daytona::SessionExecuteResponse] Command execution results containing cmd_id, output, stdout, stderr, and exit_code
147
+ #
148
+ # @example
149
+ # # Execute commands in sequence, maintaining state
150
+ # session_id = "my-session"
151
+ #
152
+ # # Change directory
153
+ # req = Daytona::SessionExecuteRequest.new(command: "cd /workspace")
154
+ # sandbox.process.execute_session_command(session_id:, req:)
155
+ #
156
+ # # Create a file
157
+ # req = Daytona::SessionExecuteRequest.new(command: "echo 'Hello' > test.txt")
158
+ # sandbox.process.execute_session_command(session_id:, req:)
159
+ #
160
+ # # Read the file
161
+ # req = Daytona::SessionExecuteRequest.new(command: "cat test.txt")
162
+ # result = sandbox.process.execute_session_command(session_id:, req:)
163
+ # puts "Command stdout: #{result.stdout}"
164
+ # puts "Command stderr: #{result.stderr}"
165
+ def execute_session_command(session_id:, req:) # rubocop:disable Metrics/MethodLength
166
+ response = toolbox_api.session_execute_command(
167
+ session_id,
168
+ DaytonaToolboxApiClient::SessionExecuteRequest.new(command: req.command, run_async: req.run_async)
169
+ )
170
+
171
+ stdout, stderr = Util.demux(response.output || '')
172
+
173
+ SessionExecuteResponse.new(
174
+ cmd_id: response.cmd_id,
175
+ output: response.output,
176
+ stdout:,
177
+ stderr:,
178
+ exit_code: response.exit_code,
179
+ # TODO: DaytonaApiClient::SessionExecuteResponse doesn't have additional_properties attribute
180
+ additional_properties: {}
181
+ )
182
+ end
183
+
184
+ # Get the logs for a command executed in a session
185
+ #
186
+ # @param session_id [String] Unique identifier of the session
187
+ # @param command_id [String] Unique identifier of the command
188
+ # @return [Daytona::SessionCommandLogsResponse] Command logs including output, stdout, and stderr
189
+ #
190
+ # @example
191
+ # logs = sandbox.process.get_session_command_logs(session_id: "my-session", command_id: "cmd-123")
192
+ # puts "Command stdout: #{logs.stdout}"
193
+ # puts "Command stderr: #{logs.stderr}"
194
+ def get_session_command_logs(session_id:, command_id:)
195
+ parse_session_command_logs(
196
+ toolbox_api.get_session_command_logs(
197
+ session_id,
198
+ command_id
199
+ )
200
+ )
201
+ end
202
+
203
+ # Asynchronously retrieves and processes the logs for a command executed in a session as they become available
204
+ #
205
+ # @param session_id [String] Unique identifier of the session
206
+ # @param command_id [String] Unique identifier of the command
207
+ # @param on_stdout [Proc] Callback function to handle stdout log chunks as they arrive
208
+ # @param on_stderr [Proc] Callback function to handle stderr log chunks as they arrive
209
+ # @return [WebSocket::Client::Simple::Client]
210
+ #
211
+ # @example
212
+ # sandbox.process.get_session_command_logs_async(
213
+ # session_id: "my-session",
214
+ # command_id: "cmd-123",
215
+ # on_stdout: ->(log) { puts "[STDOUT]: #{log}" },
216
+ # on_stderr: ->(log) { puts "[STDERR]: #{log}" }
217
+ # )
218
+ def get_session_command_logs_async(session_id:, command_id:, on_stdout:, on_stderr:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
219
+ preview_link = get_preview_link.call(WS_PORT)
220
+ url = URI.parse(preview_link.url)
221
+ url.scheme = url.scheme == 'https' ? 'wss' : 'ws'
222
+ url.path = "/process/session/#{session_id}/command/#{command_id}/logs"
223
+ url.query = 'follow=true'
224
+
225
+ WebSocket::Client::Simple.connect(
226
+ url.to_s,
227
+ headers: toolbox_api.api_client.default_headers.dup.merge(
228
+ 'X-Daytona-Preview-Token' => preview_link.token,
229
+ 'Content-Type' => 'text/plain',
230
+ 'Accept' => 'text/plain'
231
+ )
232
+ ) do |ws|
233
+ ws.on(:message) do |message|
234
+ if message.type == :close
235
+ ws.close
236
+ next
237
+ else
238
+ stdout, stderr = Util.demux(message.data.to_s)
239
+
240
+ on_stdout.call(stdout) unless stdout.empty?
241
+ on_stderr.call(stderr) unless stderr.empty?
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ #
248
+ # @return [Array<DaytonaApiClient::Session>] List of all sessions in the Sandbox
249
+ #
250
+ # @example
251
+ # sessions = sandbox.process.list_sessions
252
+ # sessions.each do |session|
253
+ # puts "Session #{session.session_id}:"
254
+ # puts " Commands: #{session.commands.length}"
255
+ # end
256
+ def list_sessions = toolbox_api.list_sessions
257
+
258
+ # Terminates and removes a session from the Sandbox, cleaning up any resources associated with it
259
+ #
260
+ # @param session_id [String] Unique identifier of the session to delete
261
+ #
262
+ # @example
263
+ # # Create and use a session
264
+ # sandbox.process.create_session("temp-session")
265
+ # # ... use the session ...
266
+ #
267
+ # # Clean up when done
268
+ # sandbox.process.delete_session("temp-session")
269
+ def delete_session(session_id) = toolbox_api.delete_session(session_id)
270
+
271
+ # Creates a new PTY (pseudo-terminal) session in the Sandbox.
272
+ #
273
+ # Creates an interactive terminal session that can execute commands and handle user input.
274
+ # The PTY session behaves like a real terminal, supporting features like command history.
275
+ #
276
+ # @param id [String] Unique identifier for the PTY session. Must be unique within the Sandbox.
277
+ # @param cwd [String, nil] Working directory for the PTY session. Defaults to the sandbox's working directory.
278
+ # @param envs [Hash<String, String>, nil] Environment variables to set in the PTY session. These will be merged with
279
+ # the Sandbox's default environment variables.
280
+ # @param pty_size [PtySize, nil] Terminal size configuration. Defaults to 80x24 if not specified.
281
+ # @return [PtyHandle] Handle for managing the created PTY session. Use this to send input,
282
+ # receive output, resize the terminal, and manage the session lifecycle.
283
+ #
284
+ # @example
285
+ # # Create a basic PTY session
286
+ # pty_handle = sandbox.process.create_pty_session(id: "my-pty")
287
+ #
288
+ # # Create a PTY session with specific size and environment
289
+ # pty_size = Daytona::PtySize.new(rows: 30, cols: 120)
290
+ # pty_handle = sandbox.process.create_pty_session(
291
+ # id: "my-pty",
292
+ # cwd: "/workspace",
293
+ # envs: {"NODE_ENV" => "development"},
294
+ # pty_size: pty_size
295
+ # )
296
+ #
297
+ # # Use the PTY session
298
+ # pty_handle.wait_for_connection
299
+ # pty_handle.send_input("ls -la\n")
300
+ # result = pty_handle.wait
301
+ # pty_handle.disconnect
302
+ #
303
+ # @raise [Daytona::Sdk::Error] If the PTY session creation fails or the session ID is already in use.
304
+ def create_pty_session(id:, cwd: nil, envs: nil, pty_size: nil) # rubocop:disable Metrics/MethodLength
305
+ response = toolbox_api.create_pty_session(
306
+ DaytonaToolboxApiClient::PtyCreateRequest.new(
307
+ id:,
308
+ cwd:,
309
+ envs:,
310
+ cols: pty_size&.cols,
311
+ rows: pty_size&.rows,
312
+ lazy_start: true
313
+ )
314
+ )
315
+
316
+ connect_pty_session(response.session_id)
317
+ end
318
+
319
+ # Connects to an existing PTY session in the Sandbox.
320
+ #
321
+ # Establishes a WebSocket connection to an existing PTY session, allowing you to
322
+ # interact with a previously created terminal session.
323
+ #
324
+ # @param session_id [String] Unique identifier of the PTY session to connect to.
325
+ # @return [PtyHandle] Handle for managing the connected PTY session.
326
+ #
327
+ # @example
328
+ # # Connect to an existing PTY session
329
+ # pty_handle = sandbox.process.connect_pty_session("my-pty-session")
330
+ # pty_handle.wait_for_connection
331
+ # pty_handle.send_input("echo 'Hello World'\n")
332
+ # result = pty_handle.wait
333
+ # pty_handle.disconnect
334
+ #
335
+ # @raise [Daytona::Sdk::Error] If the PTY session doesn't exist or connection fails.
336
+ def connect_pty_session(session_id) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
337
+ preview_link = get_preview_link.call(WS_PORT)
338
+ url = URI.parse(preview_link.url)
339
+ url.scheme = url.scheme == 'https' ? 'wss' : 'ws'
340
+ url.path = "/process/pty/#{session_id}/connect"
341
+
342
+ PtyHandle.new(
343
+ WebSocket::Client::Simple.connect(
344
+ url.to_s,
345
+ headers: toolbox_api.api_client.default_headers.dup.merge(
346
+ 'X-Daytona-Preview-Token' => preview_link.token
347
+ )
348
+ ),
349
+ session_id:,
350
+
351
+ handle_resize: ->(pty_size) { resize_pty_session(session_id, pty_size) },
352
+ handle_kill: -> { delete_pty_session(session_id) }
353
+ ).tap(&:wait_for_connection)
354
+ end
355
+
356
+ # Resizes a PTY session to the specified dimensions
357
+ #
358
+ # @param session_id [String] Unique identifier of the PTY session
359
+ # @param pty_size [PtySize] New terminal size
360
+ # @return [DaytonaApiClient::PtySessionInfo] Updated PTY session information
361
+ #
362
+ # @example
363
+ # pty_size = Daytona::PtySize.new(rows: 30, cols: 120)
364
+ # session_info = sandbox.process.resize_pty_session("my-pty", pty_size)
365
+ # puts "PTY resized to #{session_info.cols}x#{session_info.rows}"
366
+ def resize_pty_session(session_id, pty_size)
367
+ toolbox_api.resize_pty_session(
368
+ session_id,
369
+ DaytonaToolboxApiClient::PtyResizeRequest.new(
370
+ cols: pty_size.cols,
371
+ rows: pty_size.rows
372
+ )
373
+ )
374
+ end
375
+
376
+ # Deletes a PTY session, terminating the associated process
377
+ #
378
+ # @param session_id [String] Unique identifier of the PTY session to delete
379
+ # @return [void]
380
+ #
381
+ # @example
382
+ # sandbox.process.delete_pty_session("my-pty")
383
+ def delete_pty_session(session_id)
384
+ toolbox_api.delete_pty_session(session_id)
385
+ end
386
+
387
+ # Lists all PTY sessions in the Sandbox
388
+ #
389
+ # @return [Array<DaytonaApiClient::PtySessionInfo>] List of PTY session information
390
+ #
391
+ # @example
392
+ # sessions = sandbox.process.list_pty_sessions
393
+ # sessions.each do |session|
394
+ # puts "PTY Session #{session.id}: #{session.cols}x#{session.rows}"
395
+ # end
396
+ def list_pty_sessions
397
+ toolbox_api.list_pty_sessions
398
+ end
399
+
400
+ # Gets detailed information about a specific PTY session
401
+ #
402
+ # Retrieves comprehensive information about a PTY session including its current state,
403
+ # configuration, and metadata.
404
+ #
405
+ # @param session_id [String] Unique identifier of the PTY session to retrieve information for
406
+ # @return [DaytonaApiClient::PtySessionInfo] Detailed information about the PTY session including ID, state,
407
+ # creation time, working directory, environment variables, and more
408
+ #
409
+ # @example
410
+ # # Get details about a specific PTY session
411
+ # session_info = sandbox.process.get_pty_session_info("my-session")
412
+ # puts "Session ID: #{session_info.id}"
413
+ # puts "Active: #{session_info.active}"
414
+ # puts "Working Directory: #{session_info.cwd}"
415
+ # puts "Terminal Size: #{session_info.cols}x#{session_info.rows}"
416
+ def get_pty_session_info(session_id)
417
+ toolbox_api.get_pty_session(session_id)
418
+ end
419
+
420
+ private
421
+
422
+ # Parse the output of a command to extract ExecutionArtifacts
423
+ #
424
+ # @param lines [Array<String>] A list of lines of output from a command
425
+ # @return [Daytona::ExecutionArtifacts] The artifacts from the command execution
426
+ def parse_output(lines)
427
+ artifacts = ExecutionArtifacts.new('', [])
428
+
429
+ lines.each do |line|
430
+ if line.start_with?(ARTIFACT_PREFIX)
431
+ parse_json_line(line:, artifacts:)
432
+ else
433
+ artifacts.stdout += "#{line}\n"
434
+ end
435
+ end
436
+
437
+ artifacts
438
+ end
439
+
440
+ # Parse a JSON line to extract artifacts
441
+ #
442
+ # @param line [String] The line to parse
443
+ # @param artifacts [Daytona::ExecutionArtifacts] The artifacts to add to
444
+ # @return [void]
445
+ def parse_json_line(line:, artifacts:)
446
+ data = JSON.parse(line.sub(ARTIFACT_PREFIX, '').strip, symbolize_names: true)
447
+
448
+ case data.fetch(:type, nil)
449
+ when ArtifactType::CHART
450
+ artifacts.charts.append(Charts.parse(data.fetch(:value, {})))
451
+ end
452
+ end
453
+
454
+ # Parse combined stdout/stderr output into separate streams
455
+ #
456
+ # @param data [String] Combined log string with STDOUT_PREFIX and STDERR_PREFIX markers
457
+ # @return [SessionCommandLogsResponse] Response with separated stdout and stderr
458
+ def parse_session_command_logs(data)
459
+ stdout, stderr = Util.demux(data)
460
+
461
+ SessionCommandLogsResponse.new(
462
+ output: data,
463
+ stdout:,
464
+ stderr:
465
+ )
466
+ end
467
+
468
+ ARTIFACT_PREFIX = 'dtn_artifact_k39fd2:'
469
+ private_constant :ARTIFACT_PREFIX
470
+
471
+ WS_PORT = 2280
472
+ private_constant :WS_PORT
473
+
474
+ module ArtifactType
475
+ ALL = [
476
+ CHART = 'chart'
477
+ ].freeze
478
+ end
479
+ end
480
+ end