makit 0.0.140 → 0.0.141
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/README.md +41 -41
- data/exe/makit +5 -5
- data/lib/makit/apache.rb +28 -28
- data/lib/makit/auto.rb +48 -48
- data/lib/makit/cli/build_commands.rb +500 -500
- data/lib/makit/cli/generators/base_generator.rb +74 -74
- data/lib/makit/cli/generators/dotnet_generator.rb +50 -50
- data/lib/makit/cli/generators/generator_factory.rb +49 -49
- data/lib/makit/cli/generators/node_generator.rb +50 -50
- data/lib/makit/cli/generators/ruby_generator.rb +77 -77
- data/lib/makit/cli/generators/rust_generator.rb +50 -50
- data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -167
- data/lib/makit/cli/generators/templates/node_templates.rb +161 -161
- data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -26
- data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -40
- data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -33
- data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -35
- data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -63
- data/lib/makit/cli/generators/templates/ruby/test.rb +39 -39
- data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -29
- data/lib/makit/cli/generators/templates/ruby/version.rb +29 -29
- data/lib/makit/cli/generators/templates/rust_templates.rb +128 -128
- data/lib/makit/cli/main.rb +69 -69
- data/lib/makit/cli/project_commands.rb +868 -868
- data/lib/makit/cli/repository_commands.rb +661 -661
- data/lib/makit/cli/strategy_commands.rb +203 -203
- data/lib/makit/cli/utility_commands.rb +521 -521
- data/lib/makit/commands/factory.rb +359 -359
- data/lib/makit/commands/middleware/base.rb +73 -73
- data/lib/makit/commands/middleware/cache.rb +248 -248
- data/lib/makit/commands/middleware/command_logger.rb +312 -312
- data/lib/makit/commands/middleware/validator.rb +269 -269
- data/lib/makit/commands/request.rb +316 -316
- data/lib/makit/commands/result.rb +323 -323
- data/lib/makit/commands/runner.rb +388 -385
- data/lib/makit/commands/strategies/base.rb +171 -171
- data/lib/makit/commands/strategies/child_process.rb +165 -165
- data/lib/makit/commands/strategies/factory.rb +136 -136
- data/lib/makit/commands/strategies/synchronous.rb +139 -139
- data/lib/makit/commands.rb +50 -50
- data/lib/makit/configuration/dotnet_project.rb +12 -12
- data/lib/makit/configuration/gitlab_helper.rb +58 -58
- data/lib/makit/configuration/project.rb +168 -168
- data/lib/makit/configuration/rakefile_helper.rb +43 -43
- data/lib/makit/configuration/step.rb +34 -34
- data/lib/makit/configuration/timeout.rb +74 -74
- data/lib/makit/configuration.rb +15 -15
- data/lib/makit/content/default_gitignore.rb +7 -7
- data/lib/makit/content/default_gitignore.txt +225 -225
- data/lib/makit/content/default_rakefile.rb +13 -13
- data/lib/makit/content/gem_rakefile.rb +16 -16
- data/lib/makit/context.rb +1 -1
- data/lib/makit/data.rb +49 -49
- data/lib/makit/directories.rb +140 -140
- data/lib/makit/directory.rb +262 -262
- data/lib/makit/docs/files.rb +89 -89
- data/lib/makit/docs/rake.rb +102 -102
- data/lib/makit/dotnet/cli.rb +69 -69
- data/lib/makit/dotnet/project.rb +217 -217
- data/lib/makit/dotnet/solution.rb +38 -38
- data/lib/makit/dotnet/solution_classlib.rb +239 -239
- data/lib/makit/dotnet/solution_console.rb +264 -264
- data/lib/makit/dotnet/solution_maui.rb +354 -354
- data/lib/makit/dotnet/solution_wasm.rb +275 -275
- data/lib/makit/dotnet/solution_wpf.rb +304 -304
- data/lib/makit/dotnet.rb +102 -102
- data/lib/makit/email.rb +90 -90
- data/lib/makit/environment.rb +142 -142
- data/lib/makit/examples/runner.rb +370 -370
- data/lib/makit/exceptions.rb +45 -45
- data/lib/makit/fileinfo.rb +24 -24
- data/lib/makit/files.rb +43 -43
- data/lib/makit/gems.rb +40 -40
- data/lib/makit/git/cli.rb +54 -54
- data/lib/makit/git/repository.rb +90 -90
- data/lib/makit/git.rb +98 -98
- data/lib/makit/gitlab_runner.rb +59 -59
- data/lib/makit/humanize.rb +137 -137
- data/lib/makit/indexer.rb +47 -47
- data/lib/makit/logging/configuration.rb +308 -308
- data/lib/makit/logging/format_registry.rb +84 -84
- data/lib/makit/logging/formatters/base.rb +39 -39
- data/lib/makit/logging/formatters/console_formatter.rb +140 -140
- data/lib/makit/logging/formatters/json_formatter.rb +65 -65
- data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -71
- data/lib/makit/logging/formatters/text_formatter.rb +64 -64
- data/lib/makit/logging/log_request.rb +119 -119
- data/lib/makit/logging/logger.rb +199 -199
- data/lib/makit/logging/sinks/base.rb +91 -91
- data/lib/makit/logging/sinks/console.rb +72 -72
- data/lib/makit/logging/sinks/file_sink.rb +92 -92
- data/lib/makit/logging/sinks/structured.rb +123 -123
- data/lib/makit/logging/sinks/unified_file_sink.rb +296 -296
- data/lib/makit/logging.rb +565 -565
- data/lib/makit/markdown.rb +75 -75
- data/lib/makit/mp/basic_object_mp.rb +17 -17
- data/lib/makit/mp/command_mp.rb +13 -13
- data/lib/makit/mp/command_request.mp.rb +17 -17
- data/lib/makit/mp/project_mp.rb +199 -199
- data/lib/makit/mp/string_mp.rb +199 -191
- data/lib/makit/nuget.rb +74 -74
- data/lib/makit/port.rb +32 -32
- data/lib/makit/process.rb +163 -163
- data/lib/makit/protoc.rb +107 -107
- data/lib/makit/rake/cli.rb +196 -196
- data/lib/makit/rake/trace_controller.rb +173 -173
- data/lib/makit/rake.rb +80 -80
- data/lib/makit/ruby/cli.rb +185 -185
- data/lib/makit/ruby.rb +25 -25
- data/lib/makit/secrets.rb +51 -51
- data/lib/makit/serializer.rb +130 -130
- data/lib/makit/services/builder.rb +186 -186
- data/lib/makit/services/error_handler.rb +226 -226
- data/lib/makit/services/repository_manager.rb +231 -231
- data/lib/makit/services/validator.rb +112 -112
- data/lib/makit/setup/classlib.rb +101 -101
- data/lib/makit/setup/gem.rb +268 -268
- data/lib/makit/setup/razorclasslib.rb +101 -101
- data/lib/makit/setup/runner.rb +54 -54
- data/lib/makit/setup.rb +5 -5
- data/lib/makit/show.rb +110 -110
- data/lib/makit/storage.rb +126 -126
- data/lib/makit/symbols.rb +170 -170
- data/lib/makit/task_info.rb +130 -130
- data/lib/makit/tasks/at_exit.rb +15 -15
- data/lib/makit/tasks/build.rb +22 -22
- data/lib/makit/tasks/clean.rb +13 -13
- data/lib/makit/tasks/configure.rb +10 -10
- data/lib/makit/tasks/format.rb +10 -10
- data/lib/makit/tasks/hook_manager.rb +443 -443
- data/lib/makit/tasks/init.rb +49 -49
- data/lib/makit/tasks/integrate.rb +29 -29
- data/lib/makit/tasks/pull_incoming.rb +13 -13
- data/lib/makit/tasks/setup.rb +13 -13
- data/lib/makit/tasks/sync.rb +17 -17
- data/lib/makit/tasks/tag.rb +16 -16
- data/lib/makit/tasks/task_monkey_patch.rb +81 -81
- data/lib/makit/tasks/test.rb +22 -22
- data/lib/makit/tasks/update.rb +18 -18
- data/lib/makit/tasks.rb +20 -20
- data/lib/makit/test_cache.rb +239 -239
- data/lib/makit/tree.rb +37 -37
- data/lib/makit/v1/makit.v1_pb.rb +35 -35
- data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
- data/lib/makit/version.rb +99 -99
- data/lib/makit/version_util.rb +21 -21
- data/lib/makit/wix.rb +95 -95
- data/lib/makit/yaml.rb +29 -29
- data/lib/makit/zip.rb +17 -17
- data/lib/makit copy.rb +44 -44
- data/lib/makit.rb +42 -42
- metadata +2 -2
@@ -1,171 +1,171 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "open3"
|
4
|
-
require "timeout"
|
5
|
-
|
6
|
-
module Makit
|
7
|
-
module Commands
|
8
|
-
module Strategies
|
9
|
-
# Base class for command execution strategies.
|
10
|
-
#
|
11
|
-
# Execution strategies define how commands are actually executed -
|
12
|
-
# synchronously, asynchronously, in parallel, etc. This provides
|
13
|
-
# flexibility in execution patterns while maintaining a consistent
|
14
|
-
# interface.
|
15
|
-
#
|
16
|
-
# @example Creating custom strategy
|
17
|
-
# class CustomStrategy < Base
|
18
|
-
# def execute(request)
|
19
|
-
# # Custom execution logic
|
20
|
-
# result = Result.new(command: request.to_shell_command)
|
21
|
-
# # ... perform execution ...
|
22
|
-
# result.finish!(exit_code: 0, stdout: "success")
|
23
|
-
# end
|
24
|
-
# end
|
25
|
-
class Base
|
26
|
-
# Execute a command request.
|
27
|
-
#
|
28
|
-
# This method must be implemented by subclasses to provide the actual
|
29
|
-
# command execution logic. The implementation should create a Result
|
30
|
-
# object and populate it with execution details.
|
31
|
-
#
|
32
|
-
# @param request [Request] the command request to execute
|
33
|
-
# @return [Result] the execution result
|
34
|
-
# @raise [NotImplementedError] if not overridden by subclass
|
35
|
-
def execute(request)
|
36
|
-
raise NotImplementedError, "#{self.class.name} must implement #execute"
|
37
|
-
end
|
38
|
-
|
39
|
-
# Execute multiple requests (default: sequential execution).
|
40
|
-
#
|
41
|
-
# Override this method in subclasses to provide optimized batch execution
|
42
|
-
# such as parallel execution.
|
43
|
-
#
|
44
|
-
# @param requests [Array<Request>] requests to execute
|
45
|
-
# @return [Array<Result>] execution results in same order
|
46
|
-
def execute_batch(requests)
|
47
|
-
requests.map { |request| execute(request) }
|
48
|
-
end
|
49
|
-
|
50
|
-
# Check if this strategy can handle the given request.
|
51
|
-
#
|
52
|
-
# Override this method to provide conditional strategy selection
|
53
|
-
# based on request properties.
|
54
|
-
#
|
55
|
-
# @param request [Request] the command request
|
56
|
-
# @return [Boolean] true if strategy can handle the request
|
57
|
-
def supports?(_request)
|
58
|
-
true
|
59
|
-
end
|
60
|
-
|
61
|
-
# Get strategy name for logging and debugging.
|
62
|
-
#
|
63
|
-
# @return [String] strategy name
|
64
|
-
def name
|
65
|
-
self.class.name.split("::").last
|
66
|
-
end
|
67
|
-
|
68
|
-
# Get strategy configuration.
|
69
|
-
#
|
70
|
-
# @return [Hash] strategy configuration
|
71
|
-
def config
|
72
|
-
{}
|
73
|
-
end
|
74
|
-
|
75
|
-
protected
|
76
|
-
|
77
|
-
# Execute command using Open3 for cross-platform compatibility.
|
78
|
-
#
|
79
|
-
# This is a helper method that subclasses can use for actual
|
80
|
-
# system command execution.
|
81
|
-
#
|
82
|
-
# @param request [Request] the command request
|
83
|
-
# @return [Result] execution result
|
84
|
-
def execute_with_open3(request)
|
85
|
-
result = Result.new(
|
86
|
-
command: request.to_shell_command,
|
87
|
-
started_at: Time.now,
|
88
|
-
)
|
89
|
-
|
90
|
-
begin
|
91
|
-
# Change to specified directory if provided
|
92
|
-
Dir.chdir(request.directory) do
|
93
|
-
# Execute command with timeout using Timeout module
|
94
|
-
stdout, stderr, status = if request.timeout&.positive?
|
95
|
-
Timeout.timeout(request.timeout) do
|
96
|
-
Open3.capture3(
|
97
|
-
request.environment,
|
98
|
-
request.command,
|
99
|
-
*request.arguments
|
100
|
-
)
|
101
|
-
end
|
102
|
-
else
|
103
|
-
Open3.capture3(
|
104
|
-
request.environment,
|
105
|
-
request.command,
|
106
|
-
*request.arguments
|
107
|
-
)
|
108
|
-
end
|
109
|
-
|
110
|
-
result.finish!(
|
111
|
-
exit_code: status.exitstatus,
|
112
|
-
stdout: stdout,
|
113
|
-
stderr: stderr,
|
114
|
-
)
|
115
|
-
end
|
116
|
-
rescue Timeout::Error => e
|
117
|
-
result.finish!(
|
118
|
-
exit_code: 124, # timeout exit code
|
119
|
-
stderr: "Command timed out after #{request.timeout} seconds",
|
120
|
-
).add_metadata(:timeout, true)
|
121
|
-
.add_metadata(:error, e.message)
|
122
|
-
rescue StandardError => e
|
123
|
-
result.finish!(
|
124
|
-
exit_code: 1,
|
125
|
-
stderr: e.message,
|
126
|
-
).add_metadata(:error_class, e.class.name)
|
127
|
-
.add_metadata(:error, e.message)
|
128
|
-
end
|
129
|
-
|
130
|
-
result
|
131
|
-
end
|
132
|
-
|
133
|
-
# Validate that the command exists and is executable.
|
134
|
-
#
|
135
|
-
# @param command [String] command to validate
|
136
|
-
# @return [Boolean] true if command is available
|
137
|
-
def command_available?(command)
|
138
|
-
# Skip validation for dotnet commands on Windows due to path issues
|
139
|
-
return true if command == "dotnet" && RUBY_PLATFORM.match?(/mswin|mingw/)
|
140
|
-
|
141
|
-
# Try Unix-style which first
|
142
|
-
return true if system("which #{command} > /dev/null 2>&1")
|
143
|
-
|
144
|
-
# Try Windows-style where with proper quoting
|
145
|
-
return true if system("where \"#{command}\" > nul 2>&1")
|
146
|
-
|
147
|
-
# Fallback: try to execute the command directly
|
148
|
-
system("#{command} --version > nul 2>&1") ||
|
149
|
-
system("#{command} --help > nul 2>&1") ||
|
150
|
-
system("#{command} /? > nul 2>&1")
|
151
|
-
end
|
152
|
-
|
153
|
-
# Get the full path to a command.
|
154
|
-
#
|
155
|
-
# @param command [String] command name
|
156
|
-
# @return [String, nil] full path to command or nil if not found
|
157
|
-
def which(command)
|
158
|
-
# Try Unix-style which first
|
159
|
-
path = `which #{command} 2>/dev/null`.strip
|
160
|
-
return path unless path.empty?
|
161
|
-
|
162
|
-
# Try Windows-style where
|
163
|
-
path = `where #{command} 2>nul`.strip
|
164
|
-
return path unless path.empty?
|
165
|
-
|
166
|
-
nil
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
require "timeout"
|
5
|
+
|
6
|
+
module Makit
|
7
|
+
module Commands
|
8
|
+
module Strategies
|
9
|
+
# Base class for command execution strategies.
|
10
|
+
#
|
11
|
+
# Execution strategies define how commands are actually executed -
|
12
|
+
# synchronously, asynchronously, in parallel, etc. This provides
|
13
|
+
# flexibility in execution patterns while maintaining a consistent
|
14
|
+
# interface.
|
15
|
+
#
|
16
|
+
# @example Creating custom strategy
|
17
|
+
# class CustomStrategy < Base
|
18
|
+
# def execute(request)
|
19
|
+
# # Custom execution logic
|
20
|
+
# result = Result.new(command: request.to_shell_command)
|
21
|
+
# # ... perform execution ...
|
22
|
+
# result.finish!(exit_code: 0, stdout: "success")
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
class Base
|
26
|
+
# Execute a command request.
|
27
|
+
#
|
28
|
+
# This method must be implemented by subclasses to provide the actual
|
29
|
+
# command execution logic. The implementation should create a Result
|
30
|
+
# object and populate it with execution details.
|
31
|
+
#
|
32
|
+
# @param request [Request] the command request to execute
|
33
|
+
# @return [Result] the execution result
|
34
|
+
# @raise [NotImplementedError] if not overridden by subclass
|
35
|
+
def execute(request)
|
36
|
+
raise NotImplementedError, "#{self.class.name} must implement #execute"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Execute multiple requests (default: sequential execution).
|
40
|
+
#
|
41
|
+
# Override this method in subclasses to provide optimized batch execution
|
42
|
+
# such as parallel execution.
|
43
|
+
#
|
44
|
+
# @param requests [Array<Request>] requests to execute
|
45
|
+
# @return [Array<Result>] execution results in same order
|
46
|
+
def execute_batch(requests)
|
47
|
+
requests.map { |request| execute(request) }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Check if this strategy can handle the given request.
|
51
|
+
#
|
52
|
+
# Override this method to provide conditional strategy selection
|
53
|
+
# based on request properties.
|
54
|
+
#
|
55
|
+
# @param request [Request] the command request
|
56
|
+
# @return [Boolean] true if strategy can handle the request
|
57
|
+
def supports?(_request)
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get strategy name for logging and debugging.
|
62
|
+
#
|
63
|
+
# @return [String] strategy name
|
64
|
+
def name
|
65
|
+
self.class.name.split("::").last
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get strategy configuration.
|
69
|
+
#
|
70
|
+
# @return [Hash] strategy configuration
|
71
|
+
def config
|
72
|
+
{}
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
# Execute command using Open3 for cross-platform compatibility.
|
78
|
+
#
|
79
|
+
# This is a helper method that subclasses can use for actual
|
80
|
+
# system command execution.
|
81
|
+
#
|
82
|
+
# @param request [Request] the command request
|
83
|
+
# @return [Result] execution result
|
84
|
+
def execute_with_open3(request)
|
85
|
+
result = Result.new(
|
86
|
+
command: request.to_shell_command,
|
87
|
+
started_at: Time.now,
|
88
|
+
)
|
89
|
+
|
90
|
+
begin
|
91
|
+
# Change to specified directory if provided
|
92
|
+
Dir.chdir(request.directory) do
|
93
|
+
# Execute command with timeout using Timeout module
|
94
|
+
stdout, stderr, status = if request.timeout&.positive?
|
95
|
+
Timeout.timeout(request.timeout) do
|
96
|
+
Open3.capture3(
|
97
|
+
request.environment,
|
98
|
+
request.command,
|
99
|
+
*request.arguments
|
100
|
+
)
|
101
|
+
end
|
102
|
+
else
|
103
|
+
Open3.capture3(
|
104
|
+
request.environment,
|
105
|
+
request.command,
|
106
|
+
*request.arguments
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
result.finish!(
|
111
|
+
exit_code: status.exitstatus,
|
112
|
+
stdout: stdout,
|
113
|
+
stderr: stderr,
|
114
|
+
)
|
115
|
+
end
|
116
|
+
rescue Timeout::Error => e
|
117
|
+
result.finish!(
|
118
|
+
exit_code: 124, # timeout exit code
|
119
|
+
stderr: "Command timed out after #{request.timeout} seconds",
|
120
|
+
).add_metadata(:timeout, true)
|
121
|
+
.add_metadata(:error, e.message)
|
122
|
+
rescue StandardError => e
|
123
|
+
result.finish!(
|
124
|
+
exit_code: 1,
|
125
|
+
stderr: e.message,
|
126
|
+
).add_metadata(:error_class, e.class.name)
|
127
|
+
.add_metadata(:error, e.message)
|
128
|
+
end
|
129
|
+
|
130
|
+
result
|
131
|
+
end
|
132
|
+
|
133
|
+
# Validate that the command exists and is executable.
|
134
|
+
#
|
135
|
+
# @param command [String] command to validate
|
136
|
+
# @return [Boolean] true if command is available
|
137
|
+
def command_available?(command)
|
138
|
+
# Skip validation for dotnet commands on Windows due to path issues
|
139
|
+
return true if command == "dotnet" && RUBY_PLATFORM.match?(/mswin|mingw/)
|
140
|
+
|
141
|
+
# Try Unix-style which first
|
142
|
+
return true if system("which #{command} > /dev/null 2>&1")
|
143
|
+
|
144
|
+
# Try Windows-style where with proper quoting
|
145
|
+
return true if system("where \"#{command}\" > nul 2>&1")
|
146
|
+
|
147
|
+
# Fallback: try to execute the command directly
|
148
|
+
system("#{command} --version > nul 2>&1") ||
|
149
|
+
system("#{command} --help > nul 2>&1") ||
|
150
|
+
system("#{command} /? > nul 2>&1")
|
151
|
+
end
|
152
|
+
|
153
|
+
# Get the full path to a command.
|
154
|
+
#
|
155
|
+
# @param command [String] command name
|
156
|
+
# @return [String, nil] full path to command or nil if not found
|
157
|
+
def which(command)
|
158
|
+
# Try Unix-style which first
|
159
|
+
path = `which #{command} 2>/dev/null`.strip
|
160
|
+
return path unless path.empty?
|
161
|
+
|
162
|
+
# Try Windows-style where
|
163
|
+
path = `where #{command} 2>nul`.strip
|
164
|
+
return path unless path.empty?
|
165
|
+
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -1,165 +1,165 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'childprocess'
|
4
|
-
require 'tempfile'
|
5
|
-
require_relative "base"
|
6
|
-
|
7
|
-
module Makit
|
8
|
-
module Commands
|
9
|
-
module Strategies
|
10
|
-
# ChildProcess-based command execution strategy
|
11
|
-
#
|
12
|
-
# This strategy uses the ChildProcess gem for robust cross-platform
|
13
|
-
# process management. It provides better handling of timeouts, I/O,
|
14
|
-
# and process lifecycle management compared to Open3.
|
15
|
-
#
|
16
|
-
# @example Basic usage
|
17
|
-
# strategy = ChildProcess.new
|
18
|
-
# result = strategy.execute(request)
|
19
|
-
#
|
20
|
-
# @example With custom options
|
21
|
-
# strategy = ChildProcess.new(
|
22
|
-
# timeout: 60,
|
23
|
-
# max_output_size: 1_000_000
|
24
|
-
# )
|
25
|
-
class ChildProcess < Base
|
26
|
-
# @!attribute [r] timeout
|
27
|
-
# @return [Integer] default timeout in seconds
|
28
|
-
attr_reader :timeout
|
29
|
-
|
30
|
-
# @!attribute [r] max_output_size
|
31
|
-
# @return [Integer] maximum output size in bytes
|
32
|
-
attr_reader :max_output_size
|
33
|
-
|
34
|
-
# Initialize ChildProcess strategy
|
35
|
-
#
|
36
|
-
# @param timeout [Integer] default timeout in seconds (default: 30)
|
37
|
-
# @param max_output_size [Integer] maximum output size in bytes (default: 1MB)
|
38
|
-
def initialize(timeout: Makit::Configuration::Timeout.global_default, max_output_size: 1_000_000, **options)
|
39
|
-
@timeout = timeout
|
40
|
-
@max_output_size = max_output_size
|
41
|
-
super(**options)
|
42
|
-
end
|
43
|
-
|
44
|
-
# Execute a command request using ChildProcess
|
45
|
-
#
|
46
|
-
# @param request [Request] the command request to execute
|
47
|
-
# @return [Result] execution result
|
48
|
-
def execute(request)
|
49
|
-
result = Result.new(
|
50
|
-
command: request.to_shell_command,
|
51
|
-
started_at: Time.now,
|
52
|
-
)
|
53
|
-
|
54
|
-
stdout_file = nil
|
55
|
-
stderr_file = nil
|
56
|
-
process = nil
|
57
|
-
|
58
|
-
begin
|
59
|
-
# Create temporary files for output
|
60
|
-
stdout_file = Tempfile.new('makit_stdout')
|
61
|
-
stderr_file = Tempfile.new('makit_stderr')
|
62
|
-
|
63
|
-
# Build the process
|
64
|
-
process = ChildProcess.build(request.command, *request.arguments)
|
65
|
-
|
66
|
-
# Set working directory
|
67
|
-
process.cwd = request.directory if request.directory
|
68
|
-
|
69
|
-
# Set environment variables
|
70
|
-
(request.environment || {}).each do |key, value|
|
71
|
-
process.environment[key] = value.to_s
|
72
|
-
end
|
73
|
-
|
74
|
-
# Set up I/O
|
75
|
-
process.io.stdout = stdout_file
|
76
|
-
process.io.stderr = stderr_file
|
77
|
-
|
78
|
-
# Start the process
|
79
|
-
process.start
|
80
|
-
|
81
|
-
# Wait for completion with timeout
|
82
|
-
timeout_seconds = request.timeout || @timeout
|
83
|
-
process.poll_for_exit(timeout_seconds)
|
84
|
-
|
85
|
-
# Read output
|
86
|
-
stdout_file.rewind
|
87
|
-
stderr_file.rewind
|
88
|
-
stdout = stdout_file.read
|
89
|
-
stderr = stderr_file.read
|
90
|
-
|
91
|
-
# Truncate output if too large
|
92
|
-
stdout = truncate_output(stdout, "stdout")
|
93
|
-
stderr = truncate_output(stderr, "stderr")
|
94
|
-
|
95
|
-
result.finish!(
|
96
|
-
exit_code: process.exit_code,
|
97
|
-
stdout: stdout,
|
98
|
-
stderr: stderr,
|
99
|
-
)
|
100
|
-
|
101
|
-
rescue ChildProcess::TimeoutError
|
102
|
-
# Handle timeout
|
103
|
-
process&.stop
|
104
|
-
result.finish!(
|
105
|
-
exit_code: 124,
|
106
|
-
stderr: "Command timed out after #{timeout_seconds} seconds",
|
107
|
-
).add_metadata(:timeout, true)
|
108
|
-
.add_metadata(:strategy, 'childprocess')
|
109
|
-
|
110
|
-
rescue => e
|
111
|
-
# Handle other errors
|
112
|
-
process&.stop rescue nil
|
113
|
-
result.finish!(
|
114
|
-
exit_code: 1,
|
115
|
-
stderr: e.message,
|
116
|
-
).add_metadata(:error_class, e.class.name)
|
117
|
-
.add_metadata(:strategy, 'childprocess')
|
118
|
-
|
119
|
-
ensure
|
120
|
-
# Clean up resources
|
121
|
-
[stdout_file, stderr_file].each do |file|
|
122
|
-
file&.close
|
123
|
-
file&.unlink rescue nil
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
result
|
128
|
-
end
|
129
|
-
|
130
|
-
# Execute multiple requests using ChildProcess
|
131
|
-
#
|
132
|
-
# @param requests [Array<Request>] requests to execute
|
133
|
-
# @return [Array<Result>] execution results
|
134
|
-
def execute_batch(requests)
|
135
|
-
# For now, execute sequentially to avoid complexity
|
136
|
-
# Could be enhanced to use process pools in the future
|
137
|
-
requests.map { |request| execute(request) }
|
138
|
-
end
|
139
|
-
|
140
|
-
private
|
141
|
-
|
142
|
-
# Truncate output if it exceeds maximum size
|
143
|
-
#
|
144
|
-
# @param output [String] output to potentially truncate
|
145
|
-
# @param type [String] output type for logging
|
146
|
-
# @return [String] truncated output
|
147
|
-
def truncate_output(output, type)
|
148
|
-
return output if output.bytesize <= @max_output_size
|
149
|
-
|
150
|
-
original_size = output.bytesize
|
151
|
-
truncated = output.byteslice(0, @max_output_size)
|
152
|
-
|
153
|
-
Makit::Logging.warn(
|
154
|
-
"#{type.capitalize} truncated",
|
155
|
-
original_size: original_size,
|
156
|
-
max_size: @max_output_size,
|
157
|
-
strategy: 'childprocess'
|
158
|
-
)
|
159
|
-
|
160
|
-
"#{truncated}\n[#{type.upcase} TRUNCATED - Original size: #{original_size} bytes]"
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|
165
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'childprocess'
|
4
|
+
require 'tempfile'
|
5
|
+
require_relative "base"
|
6
|
+
|
7
|
+
module Makit
|
8
|
+
module Commands
|
9
|
+
module Strategies
|
10
|
+
# ChildProcess-based command execution strategy
|
11
|
+
#
|
12
|
+
# This strategy uses the ChildProcess gem for robust cross-platform
|
13
|
+
# process management. It provides better handling of timeouts, I/O,
|
14
|
+
# and process lifecycle management compared to Open3.
|
15
|
+
#
|
16
|
+
# @example Basic usage
|
17
|
+
# strategy = ChildProcess.new
|
18
|
+
# result = strategy.execute(request)
|
19
|
+
#
|
20
|
+
# @example With custom options
|
21
|
+
# strategy = ChildProcess.new(
|
22
|
+
# timeout: 60,
|
23
|
+
# max_output_size: 1_000_000
|
24
|
+
# )
|
25
|
+
class ChildProcess < Base
|
26
|
+
# @!attribute [r] timeout
|
27
|
+
# @return [Integer] default timeout in seconds
|
28
|
+
attr_reader :timeout
|
29
|
+
|
30
|
+
# @!attribute [r] max_output_size
|
31
|
+
# @return [Integer] maximum output size in bytes
|
32
|
+
attr_reader :max_output_size
|
33
|
+
|
34
|
+
# Initialize ChildProcess strategy
|
35
|
+
#
|
36
|
+
# @param timeout [Integer] default timeout in seconds (default: 30)
|
37
|
+
# @param max_output_size [Integer] maximum output size in bytes (default: 1MB)
|
38
|
+
def initialize(timeout: Makit::Configuration::Timeout.global_default, max_output_size: 1_000_000, **options)
|
39
|
+
@timeout = timeout
|
40
|
+
@max_output_size = max_output_size
|
41
|
+
super(**options)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Execute a command request using ChildProcess
|
45
|
+
#
|
46
|
+
# @param request [Request] the command request to execute
|
47
|
+
# @return [Result] execution result
|
48
|
+
def execute(request)
|
49
|
+
result = Result.new(
|
50
|
+
command: request.to_shell_command,
|
51
|
+
started_at: Time.now,
|
52
|
+
)
|
53
|
+
|
54
|
+
stdout_file = nil
|
55
|
+
stderr_file = nil
|
56
|
+
process = nil
|
57
|
+
|
58
|
+
begin
|
59
|
+
# Create temporary files for output
|
60
|
+
stdout_file = Tempfile.new('makit_stdout')
|
61
|
+
stderr_file = Tempfile.new('makit_stderr')
|
62
|
+
|
63
|
+
# Build the process
|
64
|
+
process = ChildProcess.build(request.command, *request.arguments)
|
65
|
+
|
66
|
+
# Set working directory
|
67
|
+
process.cwd = request.directory if request.directory
|
68
|
+
|
69
|
+
# Set environment variables
|
70
|
+
(request.environment || {}).each do |key, value|
|
71
|
+
process.environment[key] = value.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
# Set up I/O
|
75
|
+
process.io.stdout = stdout_file
|
76
|
+
process.io.stderr = stderr_file
|
77
|
+
|
78
|
+
# Start the process
|
79
|
+
process.start
|
80
|
+
|
81
|
+
# Wait for completion with timeout
|
82
|
+
timeout_seconds = request.timeout || @timeout
|
83
|
+
process.poll_for_exit(timeout_seconds)
|
84
|
+
|
85
|
+
# Read output
|
86
|
+
stdout_file.rewind
|
87
|
+
stderr_file.rewind
|
88
|
+
stdout = stdout_file.read
|
89
|
+
stderr = stderr_file.read
|
90
|
+
|
91
|
+
# Truncate output if too large
|
92
|
+
stdout = truncate_output(stdout, "stdout")
|
93
|
+
stderr = truncate_output(stderr, "stderr")
|
94
|
+
|
95
|
+
result.finish!(
|
96
|
+
exit_code: process.exit_code,
|
97
|
+
stdout: stdout,
|
98
|
+
stderr: stderr,
|
99
|
+
)
|
100
|
+
|
101
|
+
rescue ChildProcess::TimeoutError
|
102
|
+
# Handle timeout
|
103
|
+
process&.stop
|
104
|
+
result.finish!(
|
105
|
+
exit_code: 124,
|
106
|
+
stderr: "Command timed out after #{timeout_seconds} seconds",
|
107
|
+
).add_metadata(:timeout, true)
|
108
|
+
.add_metadata(:strategy, 'childprocess')
|
109
|
+
|
110
|
+
rescue => e
|
111
|
+
# Handle other errors
|
112
|
+
process&.stop rescue nil
|
113
|
+
result.finish!(
|
114
|
+
exit_code: 1,
|
115
|
+
stderr: e.message,
|
116
|
+
).add_metadata(:error_class, e.class.name)
|
117
|
+
.add_metadata(:strategy, 'childprocess')
|
118
|
+
|
119
|
+
ensure
|
120
|
+
# Clean up resources
|
121
|
+
[stdout_file, stderr_file].each do |file|
|
122
|
+
file&.close
|
123
|
+
file&.unlink rescue nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
result
|
128
|
+
end
|
129
|
+
|
130
|
+
# Execute multiple requests using ChildProcess
|
131
|
+
#
|
132
|
+
# @param requests [Array<Request>] requests to execute
|
133
|
+
# @return [Array<Result>] execution results
|
134
|
+
def execute_batch(requests)
|
135
|
+
# For now, execute sequentially to avoid complexity
|
136
|
+
# Could be enhanced to use process pools in the future
|
137
|
+
requests.map { |request| execute(request) }
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
# Truncate output if it exceeds maximum size
|
143
|
+
#
|
144
|
+
# @param output [String] output to potentially truncate
|
145
|
+
# @param type [String] output type for logging
|
146
|
+
# @return [String] truncated output
|
147
|
+
def truncate_output(output, type)
|
148
|
+
return output if output.bytesize <= @max_output_size
|
149
|
+
|
150
|
+
original_size = output.bytesize
|
151
|
+
truncated = output.byteslice(0, @max_output_size)
|
152
|
+
|
153
|
+
Makit::Logging.warn(
|
154
|
+
"#{type.capitalize} truncated",
|
155
|
+
original_size: original_size,
|
156
|
+
max_size: @max_output_size,
|
157
|
+
strategy: 'childprocess'
|
158
|
+
)
|
159
|
+
|
160
|
+
"#{truncated}\n[#{type.upcase} TRUNCATED - Original size: #{original_size} bytes]"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|