smart_box 0.1.1 → 0.1.3
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/CHANGELOG.md +45 -0
- data/LICENSE +21 -0
- data/lib/smart_box/box.rb +54 -59
- data/lib/smart_box/modes/copy_mode.rb +8 -7
- data/lib/smart_box/modes/git_worktree_mode.rb +22 -27
- data/lib/smart_box/runner.rb +73 -10
- data/lib/smart_box/version.rb +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8f0e87dfaff0710f069764678ec19a953f4c36af7e922f8dada50d7fd8f8f45
|
|
4
|
+
data.tar.gz: 5cd61b69354b74a0e164c95f5d4cfa62f7939f51588e03bf25e599308c81f950
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b988ffef7e71110778cdcba10131ff4d306185d307019a719bd94ed3e2218013b2290cb349df1879f2f337cedd8fa5322028dec85f7614e0b7686ca351139d6
|
|
7
|
+
data.tar.gz: b7f739bde88aa83ff2170cf2c7ec1bd13228663452dbde2737443b0058d5d17a2fdc4d05bb03cbfbce5f1092d7390dd40fdeb693fa466f08ea43999b959134cf
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.3] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Point gemspec homepage/source/changelog/bug-tracker URIs at the real
|
|
13
|
+
repository (github.com/zhuangbiaowei/smart_box).
|
|
14
|
+
- Ship the MIT `LICENSE` and `CHANGELOG.md` in the gem.
|
|
15
|
+
|
|
16
|
+
## [0.1.2] - 2026-09-14
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Eliminate process-wide `Dir.chdir` conflicts. All parent-process `Dir.chdir`
|
|
21
|
+
blocks are replaced with subprocess-level `chdir:` options, so Ruby can no
|
|
22
|
+
longer raise `conflicting chdir during another chdir block` when boxes run in
|
|
23
|
+
background threads.
|
|
24
|
+
- Enforce command timeouts. When a `timeout` is set, commands now run via
|
|
25
|
+
`Open3.popen3(pgroup: true)` and the whole process group is terminated
|
|
26
|
+
(TERM, grace period, then KILL) on expiry, returning exit code 124.
|
|
27
|
+
|
|
28
|
+
## [0.1.1] - 2026-09-13
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Use `require_relative` in `cli.rb` to avoid gem conflict warnings.
|
|
33
|
+
|
|
34
|
+
## [0.1.0] - 2026-09-13
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Initial MVP: copy and git-worktree modes, command execution with dangerous
|
|
39
|
+
command detection and logging, checkpoints, rollback, diff, export-patch,
|
|
40
|
+
apply, and discard.
|
|
41
|
+
|
|
42
|
+
[0.1.3]: https://github.com/zhuangbiaowei/smart_box/compare/v0.1.2...v0.1.3
|
|
43
|
+
[0.1.2]: https://github.com/zhuangbiaowei/smart_box/compare/v0.1.1...v0.1.2
|
|
44
|
+
[0.1.1]: https://github.com/zhuangbiaowei/smart_box/compare/v0.1.0...v0.1.1
|
|
45
|
+
[0.1.0]: https://github.com/zhuangbiaowei/smart_box/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SmartBox Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/lib/smart_box/box.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "time"
|
|
5
|
+
require "open3"
|
|
5
6
|
require_relative "errors"
|
|
6
7
|
require_relative "metadata"
|
|
7
8
|
require_relative "modes/copy_mode"
|
|
@@ -127,12 +128,11 @@ module SmartBox
|
|
|
127
128
|
def git_status_short
|
|
128
129
|
return [] unless Dir.exist?(@workspace_path)
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
out, _err, status = Open3.capture3("git", "status", "--porcelain",
|
|
132
|
+
chdir: @workspace_path)
|
|
133
|
+
return [] unless status.success?
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
end
|
|
135
|
+
out.lines.map(&:chomp).reject(&:empty?)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def run(command, env: {}, timeout: nil, allow_dangerous: false)
|
|
@@ -149,10 +149,9 @@ module SmartBox
|
|
|
149
149
|
def checkpoint(name)
|
|
150
150
|
raise Error, "Workspace does not exist" unless Dir.exist?(@workspace_path)
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
end
|
|
152
|
+
system("git", "add", "-A", chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
153
|
+
system("git", "commit", "--allow-empty", "-m", name,
|
|
154
|
+
chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
156
155
|
|
|
157
156
|
commit = git_latest_commit
|
|
158
157
|
cp_id = "cp-#{format('%03d', (@metadata.checkpoints.size + 1))}"
|
|
@@ -181,10 +180,10 @@ module SmartBox
|
|
|
181
180
|
cp = @metadata.checkpoints.detect { |c| c["id"] == checkpoint_id }
|
|
182
181
|
raise CheckpointNotFoundError, "Checkpoint '#{checkpoint_id}' not found" unless cp
|
|
183
182
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
system("git", "reset", "--hard", cp["git_commit"],
|
|
184
|
+
chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
185
|
+
system("git", "clean", "-fd",
|
|
186
|
+
chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
188
187
|
|
|
189
188
|
@metadata.load!
|
|
190
189
|
@metadata.updated_at = Time.now.utc.iso8601
|
|
@@ -196,25 +195,25 @@ module SmartBox
|
|
|
196
195
|
def diff(from: nil, to: nil)
|
|
197
196
|
raise Error, "Workspace does not exist" unless Dir.exist?(@workspace_path)
|
|
198
197
|
|
|
199
|
-
|
|
200
|
-
# Stage everything so untracked files appear in diff
|
|
201
|
-
system("git", "add", "-A", out: File::NULL, err: File::NULL)
|
|
198
|
+
system("git", "add", "-A", chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
202
199
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
200
|
+
from_commit = if from
|
|
201
|
+
resolve_checkpoint_commit(from)
|
|
202
|
+
else
|
|
203
|
+
init = @metadata.checkpoints.first
|
|
204
|
+
init ? init["git_commit"] : "HEAD~1"
|
|
205
|
+
end
|
|
209
206
|
|
|
210
|
-
|
|
207
|
+
to_commit = to ? resolve_checkpoint_commit(to) : nil
|
|
211
208
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
209
|
+
if to_commit
|
|
210
|
+
out, _err, _st = Open3.capture3("git", "diff", from_commit, to_commit,
|
|
211
|
+
chdir: @workspace_path)
|
|
212
|
+
else
|
|
213
|
+
out, _err, _st = Open3.capture3("git", "diff", "--cached", from_commit,
|
|
214
|
+
chdir: @workspace_path)
|
|
217
215
|
end
|
|
216
|
+
out.to_s
|
|
218
217
|
end
|
|
219
218
|
|
|
220
219
|
def export_patch(output:, from: nil, to: nil)
|
|
@@ -249,36 +248,29 @@ module SmartBox
|
|
|
249
248
|
backup_patch_path = File.join(@box_dir, "patches", "backup.patch")
|
|
250
249
|
FileUtils.mkdir_p(File.join(@box_dir, "patches"))
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
File.write(backup_patch_path, backup) unless backup.empty?
|
|
255
|
-
end
|
|
251
|
+
backup, _err, _st = Open3.capture3("git", "diff", chdir: @source_path)
|
|
252
|
+
File.write(backup_patch_path, backup) unless backup.empty?
|
|
256
253
|
|
|
257
254
|
# Apply the patch
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
raise PatchApplyError, "Failed to apply patch. The source project may have conflicts."
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
# Show what changed
|
|
268
|
-
result_diff = `git diff 2>/dev/null`
|
|
269
|
-
result_diff
|
|
255
|
+
_apply_out, _apply_err, status =
|
|
256
|
+
Open3.capture3("git", "apply", "-v",
|
|
257
|
+
stdin_data: patch_content, chdir: @source_path)
|
|
258
|
+
|
|
259
|
+
unless status.success?
|
|
260
|
+
raise PatchApplyError, "Failed to apply patch. The source project may have conflicts."
|
|
270
261
|
end
|
|
262
|
+
|
|
263
|
+
# Show what changed
|
|
264
|
+
result_diff, _err2, _st2 = Open3.capture3("git", "diff", chdir: @source_path)
|
|
265
|
+
result_diff
|
|
271
266
|
else
|
|
272
267
|
# Non-git source: apply patch with patch command
|
|
273
268
|
backup_patch_path = File.join(@box_dir, "patches", "backup.diff")
|
|
274
269
|
FileUtils.mkdir_p(File.join(@box_dir, "patches"))
|
|
275
270
|
File.write(backup_patch_path, "backup not available for non-git source")
|
|
276
271
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
io.write(patch_content)
|
|
280
|
-
end
|
|
281
|
-
end
|
|
272
|
+
Open3.capture3("patch", "-p1", "-N", "-r", "/dev/null",
|
|
273
|
+
stdin_data: patch_content, chdir: @source_path)
|
|
282
274
|
|
|
283
275
|
"Patch applied to non-git source at #{@source_path}"
|
|
284
276
|
end
|
|
@@ -299,7 +291,10 @@ module SmartBox
|
|
|
299
291
|
|
|
300
292
|
def source_clean?
|
|
301
293
|
return true unless Dir.exist?(File.join(@source_path, ".git"))
|
|
302
|
-
|
|
294
|
+
|
|
295
|
+
out, _err, _st = Open3.capture3("git", "status", "--porcelain",
|
|
296
|
+
chdir: @source_path)
|
|
297
|
+
out.strip.empty?
|
|
303
298
|
end
|
|
304
299
|
|
|
305
300
|
private
|
|
@@ -338,27 +333,27 @@ module SmartBox
|
|
|
338
333
|
# wrongly return "none". File.exist? is true for both files and dirs.
|
|
339
334
|
return "none" unless File.exist?(File.join(@workspace_path, ".git"))
|
|
340
335
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
336
|
+
out, _err, _st = Open3.capture3("git", "rev-parse", "HEAD",
|
|
337
|
+
chdir: @workspace_path)
|
|
338
|
+
out.strip
|
|
344
339
|
end
|
|
345
340
|
|
|
346
341
|
def source_git_commit
|
|
347
342
|
git_dir = File.join(@source_path, ".git")
|
|
348
343
|
return "none" unless Dir.exist?(git_dir)
|
|
349
344
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
345
|
+
out, _err, _st = Open3.capture3("git", "rev-parse", "HEAD",
|
|
346
|
+
chdir: @source_path)
|
|
347
|
+
out.strip
|
|
353
348
|
end
|
|
354
349
|
|
|
355
350
|
def source_git_branch
|
|
356
351
|
git_dir = File.join(@source_path, ".git")
|
|
357
352
|
return "none" unless Dir.exist?(git_dir)
|
|
358
353
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
354
|
+
out, _err, _st = Open3.capture3("git", "rev-parse", "--abbrev-ref", "HEAD",
|
|
355
|
+
chdir: @source_path)
|
|
356
|
+
out.strip
|
|
362
357
|
end
|
|
363
358
|
|
|
364
359
|
def resolve_checkpoint_commit(checkpoint_id)
|
|
@@ -93,13 +93,14 @@ module SmartBox
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def init_git!
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
system("git", "init", chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
97
|
+
system("git", "config", "user.email", "smart_box@localhost",
|
|
98
|
+
chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
99
|
+
system("git", "config", "user.name", "smart_box",
|
|
100
|
+
chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
101
|
+
system("git", "add", "-A", chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
102
|
+
system("git", "commit", "-m", "smart_box initial checkpoint",
|
|
103
|
+
chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
103
104
|
end
|
|
104
105
|
end
|
|
105
106
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
3
5
|
module SmartBox
|
|
4
6
|
module Modes
|
|
5
7
|
class GitWorktreeMode
|
|
@@ -46,25 +48,23 @@ module SmartBox
|
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def source_clean?
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
out, _err, _st = Open3.capture3("git", "status", "--porcelain",
|
|
52
|
+
chdir: @source_path)
|
|
53
|
+
out.strip.empty?
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def ensure_branch!
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
branches, _err, _st = Open3.capture3("git", "branch", "--list", @branch_name,
|
|
58
|
+
chdir: @source_path)
|
|
59
|
+
if branches.strip.empty?
|
|
60
|
+
system("git", "branch", @branch_name,
|
|
61
|
+
chdir: @source_path, out: File::NULL, err: File::NULL)
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
def create_worktree!
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
out: File::NULL, err: File::NULL)
|
|
67
|
-
end
|
|
66
|
+
system("git", "worktree", "add", @workspace_path, @branch_name,
|
|
67
|
+
chdir: @source_path, out: File::NULL, err: File::NULL)
|
|
68
68
|
|
|
69
69
|
unless Dir.exist?(@workspace_path)
|
|
70
70
|
raise SmartBox::Error, "Failed to create git worktree at #{@workspace_path}"
|
|
@@ -72,27 +72,22 @@ module SmartBox
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def init_checkpoint!
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
out: File::NULL, err: File::NULL)
|
|
79
|
-
end
|
|
75
|
+
system("git", "add", "-A", chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
76
|
+
system("git", "commit", "--allow-empty", "-m", "smart_box initial checkpoint",
|
|
77
|
+
chdir: @workspace_path, out: File::NULL, err: File::NULL)
|
|
80
78
|
end
|
|
81
79
|
|
|
82
80
|
def remove_worktree!
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
out: File::NULL, err: File::NULL)
|
|
88
|
-
end
|
|
81
|
+
system("git", "worktree", "remove", @workspace_path, "--force",
|
|
82
|
+
chdir: @source_path, out: File::NULL, err: File::NULL)
|
|
83
|
+
system("git", "branch", "-D", @branch_name,
|
|
84
|
+
chdir: @source_path, out: File::NULL, err: File::NULL)
|
|
89
85
|
end
|
|
90
86
|
|
|
91
87
|
def worktree_exists?
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
end
|
|
88
|
+
worktrees, _err, _st = Open3.capture3("git", "worktree", "list", "--porcelain",
|
|
89
|
+
chdir: @source_path)
|
|
90
|
+
worktrees.include?(@workspace_path)
|
|
96
91
|
end
|
|
97
92
|
end
|
|
98
93
|
end
|
data/lib/smart_box/runner.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "open3"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "time"
|
|
6
6
|
require "shellwords"
|
|
7
|
+
require "fileutils"
|
|
7
8
|
require_relative "command_result"
|
|
8
9
|
require_relative "errors"
|
|
9
10
|
|
|
@@ -21,6 +22,11 @@ module SmartBox
|
|
|
21
22
|
/\bchown\s+-R\b/
|
|
22
23
|
].freeze
|
|
23
24
|
|
|
25
|
+
# Exit code used when a command exceeds its timeout (GNU timeout convention).
|
|
26
|
+
TIMEOUT_EXIT_CODE = 124
|
|
27
|
+
# Grace period in seconds between TERM and KILL when terminating a process group.
|
|
28
|
+
KILL_GRACE_SECONDS = 2
|
|
29
|
+
|
|
24
30
|
attr_reader :box_id, :workspace_path, :logs_dir
|
|
25
31
|
|
|
26
32
|
def initialize(box_id:, workspace_path:, logs_dir:)
|
|
@@ -69,17 +75,25 @@ module SmartBox
|
|
|
69
75
|
stderr = ""
|
|
70
76
|
exit_code = nil
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
args = Shellwords.split(command)
|
|
79
|
+
# Never switch the parent process directory: all execution is confined to
|
|
80
|
+
# the workspace via Open3's `chdir:` option. This avoids Ruby's
|
|
81
|
+
# process-wide `conflicting chdir during another chdir block` error when
|
|
82
|
+
# boxes run in background threads alongside other threads in the process.
|
|
83
|
+
if timeout && timeout.to_f > 0
|
|
84
|
+
stdout, stderr, exit_code = execute_with_timeout(args, env, timeout.to_f)
|
|
85
|
+
else
|
|
86
|
+
begin
|
|
87
|
+
if env && !env.empty?
|
|
88
|
+
stdout, stderr, status = Open3.capture3(env, *args, chdir: @workspace_path)
|
|
89
|
+
else
|
|
90
|
+
stdout, stderr, status = Open3.capture3(*args, chdir: @workspace_path)
|
|
91
|
+
end
|
|
92
|
+
exit_code = status.exitstatus
|
|
93
|
+
rescue Errno::ENOENT => e
|
|
94
|
+
stderr = "Command not found: #{e.message}"
|
|
95
|
+
exit_code = 127
|
|
78
96
|
end
|
|
79
|
-
exit_code = status.exitstatus
|
|
80
|
-
rescue Errno::ENOENT => e
|
|
81
|
-
stderr = "Command not found: #{e.message}"
|
|
82
|
-
exit_code = 127
|
|
83
97
|
end
|
|
84
98
|
|
|
85
99
|
ended_at = Time.now
|
|
@@ -98,6 +112,55 @@ module SmartBox
|
|
|
98
112
|
)
|
|
99
113
|
end
|
|
100
114
|
|
|
115
|
+
# Runs a command with a hard timeout by spawning it in its own process group
|
|
116
|
+
# (pgroup: true) and killing the whole group on timeout. This kills child
|
|
117
|
+
# processes too (e.g. find/rsync), unlike Ruby's Timeout.timeout.
|
|
118
|
+
def execute_with_timeout(args, env, timeout_s)
|
|
119
|
+
popen_args = env && !env.empty? ? [env, *args] : args
|
|
120
|
+
Open3.popen3(*popen_args, chdir: @workspace_path, pgroup: true) do |stdin, out, err, wait_thr|
|
|
121
|
+
stdin.close
|
|
122
|
+
|
|
123
|
+
# Drain stdout/stderr concurrently to avoid pipe buffer deadlocks.
|
|
124
|
+
out_reader = Thread.new { out.read }
|
|
125
|
+
err_reader = Thread.new { err.read }
|
|
126
|
+
|
|
127
|
+
waiter = Thread.new { wait_thr.join }
|
|
128
|
+
timed_out = false
|
|
129
|
+
unless waiter.join(timeout_s)
|
|
130
|
+
timed_out = true
|
|
131
|
+
kill_process_group!(wait_thr)
|
|
132
|
+
end
|
|
133
|
+
waiter.kill if waiter.alive?
|
|
134
|
+
|
|
135
|
+
stdout = out_reader.value
|
|
136
|
+
stderr = err_reader.value
|
|
137
|
+
exit_code = timed_out ? TIMEOUT_EXIT_CODE : wait_thr.value.exitstatus
|
|
138
|
+
|
|
139
|
+
if timed_out
|
|
140
|
+
marker = "[timeout] command exceeded #{format('%g', timeout_s)}s and was killed (exit #{TIMEOUT_EXIT_CODE})"
|
|
141
|
+
stderr = stderr.to_s.empty? ? marker : "#{stderr.rstrip}\n#{marker}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
[stdout, stderr, exit_code]
|
|
145
|
+
end
|
|
146
|
+
rescue Errno::ENOENT => e
|
|
147
|
+
["", "Command not found: #{e.message}", 127]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Sends TERM to the whole process group, waits a grace period, then KILL.
|
|
151
|
+
def kill_process_group!(wait_thr)
|
|
152
|
+
%w[TERM KILL].each_with_index do |signal, idx|
|
|
153
|
+
break unless wait_thr.alive?
|
|
154
|
+
|
|
155
|
+
begin
|
|
156
|
+
Process.kill(signal, -wait_thr.pid)
|
|
157
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
158
|
+
break # Process group is already gone (or we lack permission).
|
|
159
|
+
end
|
|
160
|
+
wait_thr.join(KILL_GRACE_SECONDS) if idx.zero?
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
101
164
|
def write_logs(command, result, idx)
|
|
102
165
|
write_human_log(command, result)
|
|
103
166
|
write_jsonl_log(command, result, idx)
|
data/lib/smart_box/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smart_box
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SmartBox Team
|
|
@@ -19,6 +19,8 @@ executables:
|
|
|
19
19
|
extensions: []
|
|
20
20
|
extra_rdoc_files: []
|
|
21
21
|
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE
|
|
22
24
|
- README.md
|
|
23
25
|
- bin/smart_box
|
|
24
26
|
- lib/smart_box.rb
|
|
@@ -31,11 +33,13 @@ files:
|
|
|
31
33
|
- lib/smart_box/modes/git_worktree_mode.rb
|
|
32
34
|
- lib/smart_box/runner.rb
|
|
33
35
|
- lib/smart_box/version.rb
|
|
36
|
+
homepage: https://github.com/zhuangbiaowei/smart_box
|
|
34
37
|
licenses:
|
|
35
38
|
- MIT
|
|
36
39
|
metadata:
|
|
37
|
-
source_code_uri: https://github.com/
|
|
38
|
-
changelog_uri: https://github.com/
|
|
40
|
+
source_code_uri: https://github.com/zhuangbiaowei/smart_box
|
|
41
|
+
changelog_uri: https://github.com/zhuangbiaowei/smart_box/blob/master/CHANGELOG.md
|
|
42
|
+
bug_tracker_uri: https://github.com/zhuangbiaowei/smart_box/issues
|
|
39
43
|
rdoc_options: []
|
|
40
44
|
require_paths:
|
|
41
45
|
- lib
|
|
@@ -50,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
50
54
|
- !ruby/object:Gem::Version
|
|
51
55
|
version: '0'
|
|
52
56
|
requirements: []
|
|
53
|
-
rubygems_version: 4.0.
|
|
57
|
+
rubygems_version: 4.0.16
|
|
54
58
|
specification_version: 4
|
|
55
59
|
summary: A local reversible sandbox system for agent task execution
|
|
56
60
|
test_files: []
|