rufio 0.41.0 → 0.50.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 +4 -4
- data/README.md +64 -158
- data/bin/rufio +0 -3
- data/docs/CHANGELOG_v0.50.0.md +128 -0
- data/lib/rufio/builtin_commands.rb +34 -0
- data/lib/rufio/command_mode.rb +67 -49
- data/lib/rufio/dsl_command.rb +120 -0
- data/lib/rufio/dsl_command_loader.rb +177 -0
- data/lib/rufio/interpreter_resolver.rb +79 -0
- data/lib/rufio/script_executor.rb +253 -0
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +7 -7
- metadata +8 -8
- data/lib/rufio/plugin.rb +0 -89
- data/lib/rufio/plugin_config.rb +0 -59
- data/lib/rufio/plugin_manager.rb +0 -84
- data/lib/rufio/plugins/file_operations.rb +0 -44
- data/lib/rufio/plugins/hello.rb +0 -30
- data/lib/rufio/plugins/stop.rb +0 -32
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# DSLで定義されたコマンドを表すクラス
|
|
5
|
+
class DslCommand
|
|
6
|
+
attr_reader :name, :script, :description, :interpreter, :errors
|
|
7
|
+
attr_reader :ruby_block, :shell_command
|
|
8
|
+
|
|
9
|
+
# コマンドを初期化する
|
|
10
|
+
# @param name [String] コマンド名
|
|
11
|
+
# @param script [String, nil] スクリプトパス
|
|
12
|
+
# @param description [String] コマンドの説明
|
|
13
|
+
# @param interpreter [String, nil] インタープリタ(nilの場合は自動検出)
|
|
14
|
+
# @param ruby_block [Proc, nil] inline Rubyブロック
|
|
15
|
+
# @param shell_command [String, nil] inline シェルコマンド
|
|
16
|
+
def initialize(name:, script: nil, description: "", interpreter: nil,
|
|
17
|
+
ruby_block: nil, shell_command: nil)
|
|
18
|
+
@name = name.to_s
|
|
19
|
+
@script = script ? normalize_path(script.to_s) : nil
|
|
20
|
+
@description = description.to_s
|
|
21
|
+
@interpreter = interpreter || auto_resolve_interpreter
|
|
22
|
+
@ruby_block = ruby_block
|
|
23
|
+
@shell_command = shell_command
|
|
24
|
+
@errors = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# コマンドタイプを返す
|
|
28
|
+
# @return [Symbol] :ruby, :shell, :script のいずれか
|
|
29
|
+
def command_type
|
|
30
|
+
if @ruby_block
|
|
31
|
+
:ruby
|
|
32
|
+
elsif @shell_command
|
|
33
|
+
:shell
|
|
34
|
+
else
|
|
35
|
+
:script
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# コマンドが有効かどうかを検証する
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def valid?
|
|
42
|
+
@errors = []
|
|
43
|
+
validate_name
|
|
44
|
+
validate_execution_source
|
|
45
|
+
@errors.empty?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 実行用の引数配列を返す
|
|
49
|
+
# @return [Array<String>] [インタープリタ, スクリプトパス]
|
|
50
|
+
def to_execution_args
|
|
51
|
+
[@interpreter, @script]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# ハッシュ表現を返す
|
|
55
|
+
# @return [Hash]
|
|
56
|
+
def to_h
|
|
57
|
+
hash = {
|
|
58
|
+
name: @name,
|
|
59
|
+
script: @script,
|
|
60
|
+
description: @description,
|
|
61
|
+
interpreter: @interpreter
|
|
62
|
+
}
|
|
63
|
+
hash[:has_ruby_block] = true if @ruby_block
|
|
64
|
+
hash[:shell_command] = @shell_command if @shell_command
|
|
65
|
+
hash
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# パスを正規化する(チルダ展開、パストラバーサル解決)
|
|
71
|
+
# @param path [String] 入力パス
|
|
72
|
+
# @return [String] 正規化されたパス
|
|
73
|
+
def normalize_path(path)
|
|
74
|
+
return "" if path.empty?
|
|
75
|
+
|
|
76
|
+
# チルダを展開
|
|
77
|
+
expanded = File.expand_path(path)
|
|
78
|
+
|
|
79
|
+
# ファイルが存在する場合は実際のパスを取得(パストラバーサル解決)
|
|
80
|
+
if File.exist?(expanded)
|
|
81
|
+
File.realpath(expanded)
|
|
82
|
+
else
|
|
83
|
+
# ファイルが存在しない場合は展開されたパスをそのまま返す
|
|
84
|
+
expanded
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 拡張子からインタープリタを自動検出する
|
|
89
|
+
# @return [String, nil]
|
|
90
|
+
def auto_resolve_interpreter
|
|
91
|
+
return nil if @script.nil? || @script.empty?
|
|
92
|
+
|
|
93
|
+
InterpreterResolver.resolve_from_path(@script)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# コマンド名のバリデーション
|
|
97
|
+
def validate_name
|
|
98
|
+
if @name.empty?
|
|
99
|
+
@errors << "Command name is required"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 実行ソースのバリデーション
|
|
104
|
+
# ruby_block, shell_command, script のいずれかが必要
|
|
105
|
+
def validate_execution_source
|
|
106
|
+
# ruby_block または shell_command がある場合はスクリプト不要
|
|
107
|
+
return if @ruby_block || @shell_command
|
|
108
|
+
|
|
109
|
+
# スクリプトパスのバリデーション
|
|
110
|
+
if @script.nil? || @script.empty?
|
|
111
|
+
@errors << "Script path is required"
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
unless File.exist?(@script)
|
|
116
|
+
@errors << "Script not found: #{@script}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# DSL設定ファイルを読み込んでDslCommandを生成するクラス
|
|
5
|
+
class DslCommandLoader
|
|
6
|
+
attr_reader :errors, :warnings
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@errors = []
|
|
10
|
+
@warnings = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# 文字列からDSLを解析してコマンドをロードする
|
|
14
|
+
# @param dsl_string [String] DSL文字列
|
|
15
|
+
# @return [Array<DslCommand>] 有効なコマンドの配列
|
|
16
|
+
def load_from_string(dsl_string)
|
|
17
|
+
@errors = []
|
|
18
|
+
@warnings = []
|
|
19
|
+
|
|
20
|
+
context = DslContext.new
|
|
21
|
+
begin
|
|
22
|
+
context.instance_eval(dsl_string)
|
|
23
|
+
rescue SyntaxError, StandardError => e
|
|
24
|
+
@errors << "DSL parse error: #{e.message}"
|
|
25
|
+
return []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
validate_commands(context.commands)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# ファイルからDSLをロードする
|
|
32
|
+
# @param file_path [String] 設定ファイルのパス
|
|
33
|
+
# @return [Array<DslCommand>] 有効なコマンドの配列
|
|
34
|
+
def load_from_file(file_path)
|
|
35
|
+
@errors = []
|
|
36
|
+
@warnings = []
|
|
37
|
+
|
|
38
|
+
expanded_path = File.expand_path(file_path)
|
|
39
|
+
unless File.exist?(expanded_path)
|
|
40
|
+
return []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
content = File.read(expanded_path)
|
|
44
|
+
load_from_string(content)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 複数のパスからDSLをロードする
|
|
48
|
+
# @param paths [Array<String>] 設定ファイルのパス配列
|
|
49
|
+
# @return [Array<DslCommand>] 有効なコマンドの配列
|
|
50
|
+
def load_from_paths(paths)
|
|
51
|
+
commands = []
|
|
52
|
+
|
|
53
|
+
paths.each do |path|
|
|
54
|
+
commands.concat(load_from_file(path))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
commands
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# デフォルトのパスからDSLをロードする
|
|
61
|
+
# @return [Array<DslCommand>] 有効なコマンドの配列
|
|
62
|
+
def load
|
|
63
|
+
load_from_paths(default_config_paths)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# デフォルトの設定ファイルパスを返す
|
|
67
|
+
# @return [Array<String>] 設定ファイルパスの配列
|
|
68
|
+
def default_config_paths
|
|
69
|
+
home = Dir.home
|
|
70
|
+
[
|
|
71
|
+
File.join(home, ".rufio", "commands.rb"),
|
|
72
|
+
File.join(home, ".config", "rufio", "commands.rb")
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# コマンドをバリデーションし、有効なもののみを返す
|
|
79
|
+
# @param commands [Array<DslCommand>] コマンドの配列
|
|
80
|
+
# @return [Array<DslCommand>] 有効なコマンドの配列
|
|
81
|
+
def validate_commands(commands)
|
|
82
|
+
valid_commands = []
|
|
83
|
+
|
|
84
|
+
commands.each do |cmd|
|
|
85
|
+
if cmd.valid?
|
|
86
|
+
valid_commands << cmd
|
|
87
|
+
else
|
|
88
|
+
@warnings << "Command '#{cmd.name}' is invalid: #{cmd.errors.join(', ')}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
valid_commands
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# DSL評価用の独立したコンテキスト
|
|
96
|
+
class DslContext < BasicObject
|
|
97
|
+
attr_reader :commands
|
|
98
|
+
|
|
99
|
+
def initialize
|
|
100
|
+
@commands = []
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# コマンドを定義する
|
|
104
|
+
# @param name [String] コマンド名
|
|
105
|
+
# @yield コマンドの設定ブロック
|
|
106
|
+
def command(name, &block)
|
|
107
|
+
builder = CommandBuilder.new(name)
|
|
108
|
+
builder.instance_eval(&block) if block
|
|
109
|
+
@commands << builder.build
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 安全でないメソッドをブロック
|
|
113
|
+
def method_missing(method_name, *_args)
|
|
114
|
+
::Kernel.raise ::NoMethodError, "Method '#{method_name}' is not allowed in DSL"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# コマンドビルダー
|
|
123
|
+
class CommandBuilder
|
|
124
|
+
def initialize(name)
|
|
125
|
+
@name = name
|
|
126
|
+
@script = nil
|
|
127
|
+
@description = ""
|
|
128
|
+
@interpreter = nil
|
|
129
|
+
@ruby_block = nil
|
|
130
|
+
@shell_command = nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def script(path)
|
|
134
|
+
@script = path
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def description(desc)
|
|
138
|
+
@description = desc
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def interpreter(interp)
|
|
142
|
+
@interpreter = interp
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# inline Rubyブロックを定義
|
|
146
|
+
def ruby(&block)
|
|
147
|
+
@ruby_block = block
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# inline シェルコマンドを定義
|
|
151
|
+
def shell(command)
|
|
152
|
+
@shell_command = command
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def build
|
|
156
|
+
DslCommand.new(
|
|
157
|
+
name: @name,
|
|
158
|
+
script: @script,
|
|
159
|
+
description: @description,
|
|
160
|
+
interpreter: @interpreter,
|
|
161
|
+
ruby_block: @ruby_block,
|
|
162
|
+
shell_command: @shell_command
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 未知のメソッドは無視
|
|
167
|
+
def method_missing(_method_name, *_args)
|
|
168
|
+
# DSL内で未知のメソッドが呼ばれた場合は無視
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
|
173
|
+
true
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# 拡張子からインタープリタを解決するクラス
|
|
5
|
+
class InterpreterResolver
|
|
6
|
+
# デフォルトの拡張子とインタープリタのマッピング
|
|
7
|
+
DEFAULT_EXTENSIONS = {
|
|
8
|
+
".rb" => "ruby",
|
|
9
|
+
".py" => "python3",
|
|
10
|
+
".sh" => "bash",
|
|
11
|
+
".js" => "node",
|
|
12
|
+
".pl" => "perl",
|
|
13
|
+
".lua" => "lua",
|
|
14
|
+
".ts" => "ts-node",
|
|
15
|
+
".php" => "php",
|
|
16
|
+
".ps1" => nil # プラットフォーム依存
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# 拡張子からインタープリタを解決する
|
|
21
|
+
# @param extension [String] 拡張子(ドット付きまたはなし)
|
|
22
|
+
# @return [String, nil] インタープリタ名、解決できない場合はnil
|
|
23
|
+
def resolve(extension)
|
|
24
|
+
# ドットで始まらない場合は追加
|
|
25
|
+
ext = extension.start_with?(".") ? extension : ".#{extension}"
|
|
26
|
+
ext = ext.downcase
|
|
27
|
+
|
|
28
|
+
# PowerShellはプラットフォーム依存
|
|
29
|
+
return resolve_powershell if ext == ".ps1"
|
|
30
|
+
|
|
31
|
+
DEFAULT_EXTENSIONS[ext]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ファイルパスから拡張子を取得してインタープリタを解決する
|
|
35
|
+
# @param path [String] ファイルパス
|
|
36
|
+
# @return [String, nil] インタープリタ名、解決できない場合はnil
|
|
37
|
+
def resolve_from_path(path)
|
|
38
|
+
ext = File.extname(path)
|
|
39
|
+
return nil if ext.empty?
|
|
40
|
+
|
|
41
|
+
resolve(ext)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 全ての拡張子マッピングを取得する
|
|
45
|
+
# @return [Hash] 拡張子とインタープリタのマッピング
|
|
46
|
+
def all_extensions
|
|
47
|
+
# PowerShellのプラットフォーム依存を解決した状態で返す
|
|
48
|
+
DEFAULT_EXTENSIONS.merge(".ps1" => resolve_powershell)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Windowsプラットフォームかどうか
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def windows?
|
|
54
|
+
RUBY_PLATFORM =~ /mingw|mswin|cygwin/ ? true : false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# macOSプラットフォームかどうか
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def macos?
|
|
60
|
+
RUBY_PLATFORM =~ /darwin/ ? true : false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Linuxプラットフォームかどうか
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def linux?
|
|
66
|
+
RUBY_PLATFORM =~ /linux/ ? true : false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# PowerShellのインタープリタを解決する
|
|
72
|
+
# WindowsではpowershellとWindowsではpwsh
|
|
73
|
+
# @return [String]
|
|
74
|
+
def resolve_powershell
|
|
75
|
+
windows? ? "powershell" : "pwsh"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module Rufio
|
|
7
|
+
# スクリプトを安全に実行するクラス
|
|
8
|
+
class ScriptExecutor
|
|
9
|
+
class << self
|
|
10
|
+
# スクリプトを実行する
|
|
11
|
+
# @param interpreter [String] インタープリタ(ruby, python3, bashなど)
|
|
12
|
+
# @param script_path [String] スクリプトのパス
|
|
13
|
+
# @param args [Array<String>] スクリプトへの引数
|
|
14
|
+
# @param timeout [Numeric, nil] タイムアウト秒数(nilの場合は無制限)
|
|
15
|
+
# @param chdir [String, nil] 作業ディレクトリ
|
|
16
|
+
# @param env [Hash, nil] 環境変数
|
|
17
|
+
# @return [Hash] 実行結果
|
|
18
|
+
def execute(interpreter, script_path, args = [], timeout: nil, chdir: nil, env: nil)
|
|
19
|
+
# 配列ベースのコマンドを構築(シェルインジェクション防止)
|
|
20
|
+
command = [interpreter, script_path, *args]
|
|
21
|
+
|
|
22
|
+
# オプションを構築
|
|
23
|
+
options = {}
|
|
24
|
+
options[:chdir] = chdir if chdir
|
|
25
|
+
# 環境変数をマージ(既存の環境変数を保持)
|
|
26
|
+
spawn_env = env || {}
|
|
27
|
+
|
|
28
|
+
execute_with_options(command, spawn_env, options, timeout)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
build_error_result(e)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# DslCommandを実行する(タイプ別に分岐)
|
|
34
|
+
# @param dsl_command [DslCommand] 実行するDSLコマンド
|
|
35
|
+
# @param args [Array<String>] 追加の引数
|
|
36
|
+
# @param timeout [Numeric, nil] タイムアウト秒数
|
|
37
|
+
# @param chdir [String, nil] 作業ディレクトリ
|
|
38
|
+
# @param env [Hash, nil] 環境変数
|
|
39
|
+
# @return [Hash] 実行結果
|
|
40
|
+
def execute_command(dsl_command, args = [], timeout: nil, chdir: nil, env: nil)
|
|
41
|
+
case dsl_command.command_type
|
|
42
|
+
when :ruby
|
|
43
|
+
execute_ruby(dsl_command)
|
|
44
|
+
when :shell
|
|
45
|
+
execute_shell(dsl_command, timeout: timeout, chdir: chdir, env: env)
|
|
46
|
+
else
|
|
47
|
+
exec_args = dsl_command.to_execution_args
|
|
48
|
+
execute(exec_args[0], exec_args[1], args, timeout: timeout, chdir: chdir, env: env)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# inline Rubyコマンドを実行する
|
|
53
|
+
# @param dsl_command [DslCommand] 実行するDSLコマンド
|
|
54
|
+
# @return [Hash] 実行結果
|
|
55
|
+
def execute_ruby(dsl_command)
|
|
56
|
+
result = dsl_command.ruby_block.call
|
|
57
|
+
{
|
|
58
|
+
success: true,
|
|
59
|
+
exit_code: 0,
|
|
60
|
+
stdout: result.to_s,
|
|
61
|
+
stderr: "",
|
|
62
|
+
timeout: false
|
|
63
|
+
}
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
{
|
|
66
|
+
success: false,
|
|
67
|
+
exit_code: 1,
|
|
68
|
+
stdout: "",
|
|
69
|
+
stderr: "",
|
|
70
|
+
error: e.message,
|
|
71
|
+
timeout: false
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# inline シェルコマンドを実行する
|
|
76
|
+
# @param dsl_command [DslCommand] 実行するDSLコマンド
|
|
77
|
+
# @param timeout [Numeric, nil] タイムアウト秒数
|
|
78
|
+
# @param chdir [String, nil] 作業ディレクトリ
|
|
79
|
+
# @param env [Hash, nil] 環境変数
|
|
80
|
+
# @return [Hash] 実行結果
|
|
81
|
+
def execute_shell(dsl_command, timeout: nil, chdir: nil, env: nil)
|
|
82
|
+
options = {}
|
|
83
|
+
options[:chdir] = chdir if chdir
|
|
84
|
+
spawn_env = env || {}
|
|
85
|
+
|
|
86
|
+
if timeout
|
|
87
|
+
execute_shell_with_timeout(dsl_command.shell_command, spawn_env, options, timeout)
|
|
88
|
+
else
|
|
89
|
+
execute_shell_without_timeout(dsl_command.shell_command, spawn_env, options)
|
|
90
|
+
end
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
build_error_result(e)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# コマンドを実行し、結果を返す
|
|
98
|
+
# @param command [Array<String>] 実行するコマンド
|
|
99
|
+
# @param env [Hash] 環境変数
|
|
100
|
+
# @param options [Hash] Open3オプション
|
|
101
|
+
# @param timeout_sec [Numeric, nil] タイムアウト秒数
|
|
102
|
+
# @return [Hash] 実行結果
|
|
103
|
+
def execute_with_options(command, env, options, timeout_sec)
|
|
104
|
+
if timeout_sec
|
|
105
|
+
execute_with_timeout(command, env, options, timeout_sec)
|
|
106
|
+
else
|
|
107
|
+
execute_without_timeout(command, env, options)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# タイムアウト付きで実行
|
|
112
|
+
def execute_with_timeout(command, env, options, timeout_sec)
|
|
113
|
+
stdout = ""
|
|
114
|
+
stderr = ""
|
|
115
|
+
status = nil
|
|
116
|
+
timed_out = false
|
|
117
|
+
pid = nil
|
|
118
|
+
|
|
119
|
+
begin
|
|
120
|
+
Timeout.timeout(timeout_sec) do
|
|
121
|
+
stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, *command, **options)
|
|
122
|
+
pid = wait_thread.pid
|
|
123
|
+
stdin.close
|
|
124
|
+
stdout = stdout_io.read
|
|
125
|
+
stderr = stderr_io.read
|
|
126
|
+
stdout_io.close
|
|
127
|
+
stderr_io.close
|
|
128
|
+
status = wait_thread.value
|
|
129
|
+
end
|
|
130
|
+
rescue Timeout::Error
|
|
131
|
+
timed_out = true
|
|
132
|
+
# プロセスを終了
|
|
133
|
+
if pid
|
|
134
|
+
begin
|
|
135
|
+
Process.kill("TERM", pid)
|
|
136
|
+
sleep 0.1
|
|
137
|
+
Process.kill("KILL", pid)
|
|
138
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
139
|
+
# プロセスが既に終了している、または権限がない
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if timed_out
|
|
145
|
+
{
|
|
146
|
+
success: false,
|
|
147
|
+
exit_code: nil,
|
|
148
|
+
stdout: stdout,
|
|
149
|
+
stderr: stderr,
|
|
150
|
+
timeout: true
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
{
|
|
154
|
+
success: status&.success? || false,
|
|
155
|
+
exit_code: status&.exitstatus || 1,
|
|
156
|
+
stdout: stdout,
|
|
157
|
+
stderr: stderr,
|
|
158
|
+
timeout: false
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# タイムアウトなしで実行
|
|
164
|
+
def execute_without_timeout(command, env, options)
|
|
165
|
+
stdout, stderr, status = Open3.capture3(env, *command, **options)
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
success: status.success?,
|
|
169
|
+
exit_code: status.exitstatus,
|
|
170
|
+
stdout: stdout,
|
|
171
|
+
stderr: stderr,
|
|
172
|
+
timeout: false
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# シェルコマンドをタイムアウト付きで実行
|
|
177
|
+
def execute_shell_with_timeout(shell_command, env, options, timeout_sec)
|
|
178
|
+
stdout = ""
|
|
179
|
+
stderr = ""
|
|
180
|
+
status = nil
|
|
181
|
+
timed_out = false
|
|
182
|
+
pid = nil
|
|
183
|
+
|
|
184
|
+
begin
|
|
185
|
+
Timeout.timeout(timeout_sec) do
|
|
186
|
+
stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, shell_command, **options)
|
|
187
|
+
pid = wait_thread.pid
|
|
188
|
+
stdin.close
|
|
189
|
+
stdout = stdout_io.read
|
|
190
|
+
stderr = stderr_io.read
|
|
191
|
+
stdout_io.close
|
|
192
|
+
stderr_io.close
|
|
193
|
+
status = wait_thread.value
|
|
194
|
+
end
|
|
195
|
+
rescue Timeout::Error
|
|
196
|
+
timed_out = true
|
|
197
|
+
if pid
|
|
198
|
+
begin
|
|
199
|
+
Process.kill("TERM", pid)
|
|
200
|
+
sleep 0.1
|
|
201
|
+
Process.kill("KILL", pid)
|
|
202
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
203
|
+
# プロセスが既に終了している、または権限がない
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if timed_out
|
|
209
|
+
{
|
|
210
|
+
success: false,
|
|
211
|
+
exit_code: nil,
|
|
212
|
+
stdout: stdout,
|
|
213
|
+
stderr: stderr,
|
|
214
|
+
timeout: true
|
|
215
|
+
}
|
|
216
|
+
else
|
|
217
|
+
{
|
|
218
|
+
success: status&.success? || false,
|
|
219
|
+
exit_code: status&.exitstatus || 1,
|
|
220
|
+
stdout: stdout,
|
|
221
|
+
stderr: stderr,
|
|
222
|
+
timeout: false
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# シェルコマンドをタイムアウトなしで実行
|
|
228
|
+
def execute_shell_without_timeout(shell_command, env, options)
|
|
229
|
+
stdout, stderr, status = Open3.capture3(env, shell_command, **options)
|
|
230
|
+
|
|
231
|
+
{
|
|
232
|
+
success: status.success?,
|
|
233
|
+
exit_code: status.exitstatus,
|
|
234
|
+
stdout: stdout,
|
|
235
|
+
stderr: stderr,
|
|
236
|
+
timeout: false
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# エラー結果を構築
|
|
241
|
+
def build_error_result(error)
|
|
242
|
+
{
|
|
243
|
+
success: false,
|
|
244
|
+
exit_code: 1,
|
|
245
|
+
stdout: "",
|
|
246
|
+
stderr: "",
|
|
247
|
+
error: error.message,
|
|
248
|
+
timeout: false
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
data/lib/rufio/version.rb
CHANGED
data/lib/rufio.rb
CHANGED
|
@@ -21,10 +21,13 @@ require_relative "rufio/application"
|
|
|
21
21
|
require_relative "rufio/file_opener"
|
|
22
22
|
require_relative "rufio/health_checker"
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
require_relative "rufio/
|
|
26
|
-
require_relative "rufio/
|
|
27
|
-
require_relative "rufio/
|
|
24
|
+
# DSLコマンドシステム
|
|
25
|
+
require_relative "rufio/interpreter_resolver"
|
|
26
|
+
require_relative "rufio/dsl_command"
|
|
27
|
+
require_relative "rufio/script_executor"
|
|
28
|
+
require_relative "rufio/dsl_command_loader"
|
|
29
|
+
require_relative "rufio/builtin_commands"
|
|
30
|
+
|
|
28
31
|
require_relative "rufio/command_mode"
|
|
29
32
|
require_relative "rufio/command_mode_ui"
|
|
30
33
|
require_relative "rufio/command_history"
|
|
@@ -45,9 +48,6 @@ require_relative "rufio/project_mode"
|
|
|
45
48
|
require_relative "rufio/project_command"
|
|
46
49
|
require_relative "rufio/project_log"
|
|
47
50
|
|
|
48
|
-
# プラグインをロード
|
|
49
|
-
Rufio::PluginManager.load_all
|
|
50
|
-
|
|
51
51
|
module Rufio
|
|
52
52
|
class Error < StandardError; end
|
|
53
53
|
end
|