rush-shell 0.0.2
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/exe/rush +10 -0
- data/grammar/shell.y +272 -0
- data/grammar/shell.y.output +2354 -0
- data/lib/rush/alias_table.rb +48 -0
- data/lib/rush/assignments.rb +48 -0
- data/lib/rush/ast/and_or.rb +37 -0
- data/lib/rush/ast/assignment.rb +20 -0
- data/lib/rush/ast/brace_group.rb +25 -0
- data/lib/rush/ast/case_node.rb +41 -0
- data/lib/rush/ast/condition_loop.rb +34 -0
- data/lib/rush/ast/for_node.rb +34 -0
- data/lib/rush/ast/function_def.rb +26 -0
- data/lib/rush/ast/if_node.rb +29 -0
- data/lib/rush/ast/list.rb +47 -0
- data/lib/rush/ast/node.rb +17 -0
- data/lib/rush/ast/param_ref.rb +59 -0
- data/lib/rush/ast/pipeline.rb +45 -0
- data/lib/rush/ast/redirect.rb +21 -0
- data/lib/rush/ast/redirected.rb +26 -0
- data/lib/rush/ast/simple_command.rb +62 -0
- data/lib/rush/ast/subshell.rb +24 -0
- data/lib/rush/ast/until_node.rb +18 -0
- data/lib/rush/ast/while_node.rb +18 -0
- data/lib/rush/ast/word.rb +44 -0
- data/lib/rush/ast/word_segment.rb +203 -0
- data/lib/rush/background_runner.rb +68 -0
- data/lib/rush/bracket_expression.rb +107 -0
- data/lib/rush/bracket_scanner.rb +64 -0
- data/lib/rush/builtins/alias_.rb +74 -0
- data/lib/rush/builtins/base.rb +96 -0
- data/lib/rush/builtins/break_.rb +19 -0
- data/lib/rush/builtins/cd.rb +127 -0
- data/lib/rush/builtins/colon.rb +16 -0
- data/lib/rush/builtins/command.rb +137 -0
- data/lib/rush/builtins/command_options.rb +103 -0
- data/lib/rush/builtins/continue_.rb +19 -0
- data/lib/rush/builtins/declare.rb +27 -0
- data/lib/rush/builtins/defaults.rb +28 -0
- data/lib/rush/builtins/dot.rb +52 -0
- data/lib/rush/builtins/echo.rb +37 -0
- data/lib/rush/builtins/eval.rb +25 -0
- data/lib/rush/builtins/exec.rb +54 -0
- data/lib/rush/builtins/exit.rb +45 -0
- data/lib/rush/builtins/export.rb +20 -0
- data/lib/rush/builtins/false_.rb +16 -0
- data/lib/rush/builtins/fd_operand.rb +87 -0
- data/lib/rush/builtins/getopts.rb +96 -0
- data/lib/rush/builtins/hash.rb +64 -0
- data/lib/rush/builtins/job_resume.rb +107 -0
- data/lib/rush/builtins/jobs.rb +86 -0
- data/lib/rush/builtins/kill.rb +152 -0
- data/lib/rush/builtins/local.rb +40 -0
- data/lib/rush/builtins/loop_jump.rb +45 -0
- data/lib/rush/builtins/printf.rb +39 -0
- data/lib/rush/builtins/printf_formatter.rb +111 -0
- data/lib/rush/builtins/pwd.rb +17 -0
- data/lib/rush/builtins/read.rb +93 -0
- data/lib/rush/builtins/read_input.rb +146 -0
- data/lib/rush/builtins/readonly.rb +20 -0
- data/lib/rush/builtins/registry.rb +36 -0
- data/lib/rush/builtins/return_.rb +24 -0
- data/lib/rush/builtins/set.rb +101 -0
- data/lib/rush/builtins/shift.rb +35 -0
- data/lib/rush/builtins/test_.rb +48 -0
- data/lib/rush/builtins/test_context.rb +24 -0
- data/lib/rush/builtins/test_expr.rb +74 -0
- data/lib/rush/builtins/test_grammar.rb +117 -0
- data/lib/rush/builtins/test_operators.rb +94 -0
- data/lib/rush/builtins/test_tokens.rb +69 -0
- data/lib/rush/builtins/times.rb +35 -0
- data/lib/rush/builtins/trap.rb +85 -0
- data/lib/rush/builtins/true_.rb +16 -0
- data/lib/rush/builtins/type_.rb +28 -0
- data/lib/rush/builtins/ulimit.rb +258 -0
- data/lib/rush/builtins/umask.rb +81 -0
- data/lib/rush/builtins/unalias.rb +46 -0
- data/lib/rush/builtins/unset.rb +40 -0
- data/lib/rush/builtins/wait.rb +127 -0
- data/lib/rush/cli.rb +31 -0
- data/lib/rush/command_assignments.rb +56 -0
- data/lib/rush/command_lookup.rb +228 -0
- data/lib/rush/command_resolution.rb +115 -0
- data/lib/rush/command_runner.rb +152 -0
- data/lib/rush/command_text.rb +140 -0
- data/lib/rush/environment.rb +134 -0
- data/lib/rush/errexit_context.rb +68 -0
- data/lib/rush/error_policy.rb +64 -0
- data/lib/rush/errors.rb +100 -0
- data/lib/rush/escape_table.rb +22 -0
- data/lib/rush/escaped_bracket.rb +49 -0
- data/lib/rush/executor.rb +164 -0
- data/lib/rush/exit_trap.rb +109 -0
- data/lib/rush/expansion/arithmetic/evaluator.rb +43 -0
- data/lib/rush/expansion/arithmetic/nodes.rb +180 -0
- data/lib/rush/expansion/arithmetic/number.rb +104 -0
- data/lib/rush/expansion/arithmetic/parser.rb +155 -0
- data/lib/rush/expansion/arithmetic/tokenizer.rb +56 -0
- data/lib/rush/expansion/arithmetic_expander.rb +38 -0
- data/lib/rush/expansion/command_substitution.rb +87 -0
- data/lib/rush/expansion/field_part.rb +12 -0
- data/lib/rush/expansion/field_splitter.rb +23 -0
- data/lib/rush/expansion/glob_expander.rb +48 -0
- data/lib/rush/expansion/ifs.rb +62 -0
- data/lib/rush/expansion/ifs_scanner.rb +168 -0
- data/lib/rush/expansion/parameter_expander.rb +204 -0
- data/lib/rush/expansion/parameter_forms.rb +22 -0
- data/lib/rush/expansion/pattern_removal.rb +63 -0
- data/lib/rush/expansion/pipeline.rb +124 -0
- data/lib/rush/expansion/read_char.rb +13 -0
- data/lib/rush/expansion/read_field_scanner.rb +159 -0
- data/lib/rush/expansion/read_splitter.rb +40 -0
- data/lib/rush/expansion/resolver.rb +23 -0
- data/lib/rush/expansion/tilde_expander.rb +102 -0
- data/lib/rush/external.rb +75 -0
- data/lib/rush/fd_entry.rb +60 -0
- data/lib/rush/for_runner.rb +50 -0
- data/lib/rush/function_frame.rb +30 -0
- data/lib/rush/function_runner.rb +37 -0
- data/lib/rush/function_table.rb +35 -0
- data/lib/rush/getopts_parser.rb +195 -0
- data/lib/rush/getopts_state.rb +104 -0
- data/lib/rush/here_doc.rb +57 -0
- data/lib/rush/interactive_signals.rb +26 -0
- data/lib/rush/invocation.rb +190 -0
- data/lib/rush/io_table.rb +94 -0
- data/lib/rush/job_control.rb +197 -0
- data/lib/rush/job_report.rb +45 -0
- data/lib/rush/job_spec.rb +51 -0
- data/lib/rush/job_table/control.rb +103 -0
- data/lib/rush/job_table/interruptible_wait.rb +70 -0
- data/lib/rush/job_table/job.rb +181 -0
- data/lib/rush/job_table.rb +194 -0
- data/lib/rush/lexer/alias_expander.rb +129 -0
- data/lib/rush/lexer/braced_reader.rb +86 -0
- data/lib/rush/lexer/case_frame.rb +114 -0
- data/lib/rush/lexer/case_tracker.rb +146 -0
- data/lib/rush/lexer/dollar_scanner.rb +53 -0
- data/lib/rush/lexer/double_quote_scanner.rb +71 -0
- data/lib/rush/lexer/heredoc_body.rb +91 -0
- data/lib/rush/lexer/heredoc_reader.rb +93 -0
- data/lib/rush/lexer/lex_state.rb +128 -0
- data/lib/rush/lexer/operator_table.rb +32 -0
- data/lib/rush/lexer/param_scanner.rb +43 -0
- data/lib/rush/lexer/paren_reader.rb +113 -0
- data/lib/rush/lexer/paren_regions.rb +74 -0
- data/lib/rush/lexer/quote_skips.rb +77 -0
- data/lib/rush/lexer/quoted_word.rb +67 -0
- data/lib/rush/lexer/scanner_predicates.rb +20 -0
- data/lib/rush/lexer/source_lines.rb +43 -0
- data/lib/rush/lexer/substitution_reader.rb +77 -0
- data/lib/rush/lexer/token_classifier.rb +144 -0
- data/lib/rush/lexer/token_predicates.rb +23 -0
- data/lib/rush/lexer/word_scanner.rb +134 -0
- data/lib/rush/lexer.rb +174 -0
- data/lib/rush/loop_control_handling.rb +29 -0
- data/lib/rush/loop_nesting.rb +50 -0
- data/lib/rush/loop_runner.rb +55 -0
- data/lib/rush/option_cluster.rb +39 -0
- data/lib/rush/options.rb +76 -0
- data/lib/rush/param_text.rb +39 -0
- data/lib/rush/parser.rb +1213 -0
- data/lib/rush/parser_support.rb +112 -0
- data/lib/rush/pattern_scanner.rb +35 -0
- data/lib/rush/pending_signals.rb +57 -0
- data/lib/rush/pipeline_runner.rb +173 -0
- data/lib/rush/pipeline_statuses.rb +45 -0
- data/lib/rush/positional.rb +45 -0
- data/lib/rush/posix_pattern.rb +78 -0
- data/lib/rush/program_input.rb +89 -0
- data/lib/rush/program_reader.rb +69 -0
- data/lib/rush/program_session.rb +83 -0
- data/lib/rush/prompt.rb +52 -0
- data/lib/rush/redirect_scope.rb +64 -0
- data/lib/rush/redirection/dup_redirect.rb +58 -0
- data/lib/rush/redirection/file_redirect.rb +50 -0
- data/lib/rush/redirection/here_doc_redirect.rb +17 -0
- data/lib/rush/redirection/registry.rb +53 -0
- data/lib/rush/repl.rb +133 -0
- data/lib/rush/runtime_type_checks.rb +16 -0
- data/lib/rush/scope.rb +79 -0
- data/lib/rush/segment_buffer.rb +47 -0
- data/lib/rush/shell_parameters.rb +77 -0
- data/lib/rush/shell_pattern.rb +80 -0
- data/lib/rush/shell_state.rb +163 -0
- data/lib/rush/shell_variables.rb +67 -0
- data/lib/rush/signal_report.rb +41 -0
- data/lib/rush/signals.rb +68 -0
- data/lib/rush/source.rb +82 -0
- data/lib/rush/source_line_counter.rb +41 -0
- data/lib/rush/source_runner.rb +47 -0
- data/lib/rush/startup.rb +75 -0
- data/lib/rush/status.rb +93 -0
- data/lib/rush/stop_relay.rb +32 -0
- data/lib/rush/subshell_runner.rb +70 -0
- data/lib/rush/system_calls/collation.rb +155 -0
- data/lib/rush/system_calls/file_tests.rb +95 -0
- data/lib/rush/system_calls/process_control.rb +165 -0
- data/lib/rush/system_calls/process_identity.rb +43 -0
- data/lib/rush/system_calls/resource_limits.rb +51 -0
- data/lib/rush/system_calls.rb +206 -0
- data/lib/rush/terminal.rb +126 -0
- data/lib/rush/trap_runner.rb +169 -0
- data/lib/rush/trap_table.rb +49 -0
- data/lib/rush/umask_mode.rb +133 -0
- data/lib/rush/version.rb +6 -0
- data/lib/rush.rb +190 -0
- metadata +527 -0
data/lib/rush/startup.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# The startup files a session runs before its main input, per the dash
|
|
6
|
+
# oracle (POSIX defines ENV and leaves login files unspecified): a login
|
|
7
|
+
# shell runs /etc/profile then $HOME/.profile; an interactive shell then
|
|
8
|
+
# runs the file named by ENV, its value subjected to parameter expansion
|
|
9
|
+
# only. Missing or unreadable files are skipped silently; errors inside a
|
|
10
|
+
# file follow the session's normal policy (a batch shell aborts, an
|
|
11
|
+
# interactive one reports and carries on); `return` is bounded to its file
|
|
12
|
+
# like a dot script, leaving its code in $?.
|
|
13
|
+
class Startup
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
PROFILE = '/etc/profile'
|
|
17
|
+
|
|
18
|
+
sig { params(login: T::Boolean, interactive: T::Boolean).void }
|
|
19
|
+
def initialize(login:, interactive:)
|
|
20
|
+
@login = login
|
|
21
|
+
@interactive = interactive
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(executor: Executor).void }
|
|
25
|
+
def run(executor)
|
|
26
|
+
paths(executor).each { |path| source(executor, path) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
sig { params(executor: Executor).returns(T::Array[String]) }
|
|
32
|
+
def paths(executor)
|
|
33
|
+
paths = @login ? [PROFILE, home_profile(executor)] : [] #: Array[String?]
|
|
34
|
+
paths << env_file(executor) if @interactive
|
|
35
|
+
paths.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { params(executor: Executor).returns(T.nilable(String)) }
|
|
39
|
+
def home_profile(executor)
|
|
40
|
+
home = executor.state.variables.get('HOME').to_s
|
|
41
|
+
return if home.empty?
|
|
42
|
+
|
|
43
|
+
"#{home}/.profile"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The ENV pathname, parameter-expanded (POSIX); nil when unset or empty.
|
|
47
|
+
sig { params(executor: Executor).returns(T.nilable(String)) }
|
|
48
|
+
def env_file(executor)
|
|
49
|
+
raw = executor.state.variables.get('ENV').to_s
|
|
50
|
+
return if raw.empty?
|
|
51
|
+
|
|
52
|
+
executor.expander.expand_value(ParamText.new(raw).word, tilde: :none)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(executor: Executor, path: String).void }
|
|
56
|
+
def source(executor, path)
|
|
57
|
+
text = read(executor, path)
|
|
58
|
+
run_file(executor, text) if text
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { params(executor: Executor, path: String).returns(T.nilable(String)) }
|
|
62
|
+
def read(executor, path)
|
|
63
|
+
executor.system.read_file(path)
|
|
64
|
+
rescue SystemCallError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { params(executor: Executor, text: String).void }
|
|
69
|
+
def run_file(executor, text)
|
|
70
|
+
SourceRunner.new(executor, text).run
|
|
71
|
+
rescue ReturnSignal => e
|
|
72
|
+
executor.state.record_status(Status.new(e.code))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/rush/status.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Immutable result of running a command. $? can hold a value wider than a byte:
|
|
6
|
+
# dash keeps `return 300` as 300 in-process, and only wraps to 0-255 at a real
|
|
7
|
+
# process boundary (exit!/the shell's own exit, where the OS truncates). The
|
|
8
|
+
# operand is validated non-negative and <= INT_MAX before reaching here.
|
|
9
|
+
# Under job control a WUNTRACED wait can answer for a job that STOPPED rather
|
|
10
|
+
# than ended: stopsig carries the stopping signal, and the exitstatus is
|
|
11
|
+
# dash's view of the pair — normally 128+stopsig, but a mixed pipeline keeps
|
|
12
|
+
# the last stage's exit code while the job as a whole is stopped (probed).
|
|
13
|
+
class Status
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
sig { returns(Integer) }
|
|
17
|
+
attr_reader :exitstatus
|
|
18
|
+
|
|
19
|
+
sig { returns(T.nilable(Integer)) }
|
|
20
|
+
attr_reader :stopsig
|
|
21
|
+
|
|
22
|
+
sig { returns(T.nilable(Integer)) }
|
|
23
|
+
attr_reader :termsig
|
|
24
|
+
|
|
25
|
+
sig do
|
|
26
|
+
params(exitstatus: Integer, stopsig: T.nilable(Integer), termsig: T.nilable(Integer),
|
|
27
|
+
coredump: T.nilable(T::Boolean)).void
|
|
28
|
+
end
|
|
29
|
+
def initialize(exitstatus, stopsig: nil, termsig: nil, coredump: nil)
|
|
30
|
+
@exitstatus = exitstatus
|
|
31
|
+
@stopsig = stopsig
|
|
32
|
+
@termsig = termsig
|
|
33
|
+
@coredump = coredump
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The OS recorded a core dump alongside the killing signal (WCOREDUMP,
|
|
37
|
+
# nil when no dump information accompanied the status): SignalReport
|
|
38
|
+
# suffixes its message with " (core dumped)" like dash.
|
|
39
|
+
sig { returns(T::Boolean) }
|
|
40
|
+
def coredump?
|
|
41
|
+
!!@coredump
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { returns(T::Boolean) }
|
|
45
|
+
def success?
|
|
46
|
+
exitstatus.zero?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The job this status describes is stopped, not gone: the foreground
|
|
50
|
+
# wait hands it back to the prompt as a Stopped job-table entry.
|
|
51
|
+
sig { returns(T::Boolean) }
|
|
52
|
+
def stopped?
|
|
53
|
+
!!@stopsig
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The same verdict, carrying a sibling stage's stop signal: dash keeps
|
|
57
|
+
# the last stage's exit code even when an earlier stage's ^Z parked the
|
|
58
|
+
# whole pipeline (probed: stopped|exit5 answers 5, with the job
|
|
59
|
+
# Stopped), so the job's stoppedness rides along on the chosen status.
|
|
60
|
+
sig { params(stopsig: T.nilable(Integer)).returns(Status) }
|
|
61
|
+
def with_stop(stopsig)
|
|
62
|
+
stopped? || !stopsig ? self : Status.new(exitstatus, stopsig: stopsig)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { returns(Status) }
|
|
66
|
+
def self.success
|
|
67
|
+
new(0)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { params(code: Integer).returns(Status) }
|
|
71
|
+
def self.failure(code = 1)
|
|
72
|
+
new(code)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A signalled process reports no exitstatus; POSIX maps it to 128 + signal.
|
|
76
|
+
# termsig is Integer? to the type-checker (nil unless signalled); the signalled
|
|
77
|
+
# branch here is the only one reached when exitstatus is nil, so .to_i pins it
|
|
78
|
+
# to a plain Integer without changing behaviour on any reachable path. A
|
|
79
|
+
# stopped child (WUNTRACED) maps to 128 + stopsig with the signal kept.
|
|
80
|
+
sig { params(process_status: Process::Status).returns(Status) }
|
|
81
|
+
def self.of(process_status)
|
|
82
|
+
return stopped(process_status.stopsig.to_i) if process_status.stopped?
|
|
83
|
+
|
|
84
|
+
signal = process_status.termsig
|
|
85
|
+
new(process_status.exitstatus || (signal.to_i + 128), termsig: signal, coredump: process_status.coredump?)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig { params(stopsig: Integer).returns(Status) }
|
|
89
|
+
def self.stopped(stopsig)
|
|
90
|
+
new(stopsig + 128, stopsig: stopsig)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# The armed stage relay (rush-l4o): a pipeline stage of a monitor-mode job
|
|
6
|
+
# is a transparent member of it — a stop its own foreground wait reaps is
|
|
7
|
+
# re-raised onto the stage process itself, so the job's owner sees the
|
|
8
|
+
# member stop; after fg/bg's SIGCONT the reap loop re-waits the same
|
|
9
|
+
# target. dash reaches the same picture by exec-ing simple stages in
|
|
10
|
+
# place. Methods are explicit singletons, like Signals.
|
|
11
|
+
module StopRelay
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
# The reaped target's stop should be re-raised: relay armed AND the
|
|
15
|
+
# status is a stop (deaths and exits pass through untouched).
|
|
16
|
+
sig { params(control: JobTable::Control, status: Process::Status).returns(T::Boolean) }
|
|
17
|
+
def self.relay?(control, status)
|
|
18
|
+
control.relay? && status.stopped?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Default disposition first — the -m parent left TSTP/TTOU ignored,
|
|
22
|
+
# while SIGSTOP takes no disposition and cannot be trapped — then the
|
|
23
|
+
# stop signal onto this very process. The block never runs: the
|
|
24
|
+
# command string wins inside trap_signal, as TrapRunner's ignores do.
|
|
25
|
+
sig { params(system: SystemCalls, status: Process::Status).void }
|
|
26
|
+
def self.raise_onto_self(system, status)
|
|
27
|
+
name = Signals::NUMBERS.fetch(T.must(status.stopsig))
|
|
28
|
+
system.trap_signal(name, 'SYSTEM_DEFAULT') { nil } unless name == 'STOP'
|
|
29
|
+
system.kill(name, system.pid)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
# Runs `( list )` in a forked subshell so cd/variable changes never leak to the
|
|
6
|
+
# parent. The child runs the body and exits with its status; the parent waits
|
|
7
|
+
# and adopts that status. fork/exit! are the one irreducible wrapper (covered
|
|
8
|
+
# by subprocess specs); the child-side `run_body` is tested directly.
|
|
9
|
+
class SubshellRunner
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(executor: Executor, body: AST::Node).void }
|
|
13
|
+
def initialize(executor, body)
|
|
14
|
+
@executor = executor
|
|
15
|
+
@body = body
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { returns(Status) }
|
|
19
|
+
def call
|
|
20
|
+
control = @executor.job_control
|
|
21
|
+
pid = control.launch { run_child }
|
|
22
|
+
text = control.job_text(AST::Subshell.new(@body))
|
|
23
|
+
status = control.foreground([pid], text: text) { @executor.jobs.await(pid) }
|
|
24
|
+
SignalReport.report(status, @executor.io.get(2))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The subshell is a fresh top level: exit (or an uncaught return) ends it
|
|
28
|
+
# with that code, a stray break/continue is a no-op, and a fatal error
|
|
29
|
+
# (readonly, ${x:?}, ...) aborts only the subshell — the parent shell carries
|
|
30
|
+
# on — without letting the exception escape the fork.
|
|
31
|
+
sig { returns(Status) }
|
|
32
|
+
def run_body
|
|
33
|
+
@executor.enter_subshell
|
|
34
|
+
status = run_body_status
|
|
35
|
+
Status.new(@executor.trap_runner.run_exit_trap(status.exitstatus))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
sig { returns(Status) }
|
|
41
|
+
def run_body_status
|
|
42
|
+
@executor.run(@body)
|
|
43
|
+
rescue Error => e
|
|
44
|
+
resolve(e)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { params(error: Error).returns(Status) }
|
|
48
|
+
def resolve(error)
|
|
49
|
+
decision = ErrorPolicy.decision(:subshell, error)
|
|
50
|
+
return Status.new(T.cast(error, T.any(ExitSignal, ReturnSignal)).code) if decision == :return_code
|
|
51
|
+
return @executor.state.last_status if decision == :last_status
|
|
52
|
+
return report_fatal(error) if decision == :abort2
|
|
53
|
+
|
|
54
|
+
raise error
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { params(error: Error).returns(Status) }
|
|
58
|
+
def report_fatal(error)
|
|
59
|
+
@executor.io.get(2).puts("rush: #{error.message}")
|
|
60
|
+
Status.failure(2)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { returns(T.noreturn) }
|
|
64
|
+
def run_child
|
|
65
|
+
# :nocov:
|
|
66
|
+
@executor.system.exit!(run_body.exitstatus)
|
|
67
|
+
# :nocov:
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'fiddle'
|
|
5
|
+
|
|
6
|
+
module Rush
|
|
7
|
+
class SystemCalls
|
|
8
|
+
# Optional glibc bridge for the locale data Ruby does not expose. POSIX ERE
|
|
9
|
+
# matching supplies bracket classes/equivalence/collating elements/ranges;
|
|
10
|
+
# strcoll supplies pathname order. The fixed regex buffer is deliberately
|
|
11
|
+
# glibc-gated (regex_t is opaque at the Fiddle boundary).
|
|
12
|
+
# One cohesive native-resource boundary; splitting function loading from
|
|
13
|
+
# regex ownership would separate lifecycle halves only to satisfy a metric.
|
|
14
|
+
# rubocop:disable Metrics/ClassLength
|
|
15
|
+
class Collation
|
|
16
|
+
extend T::Sig
|
|
17
|
+
|
|
18
|
+
LC_CTYPE = 0
|
|
19
|
+
LC_COLLATE = 3
|
|
20
|
+
REG_EXTENDED = 1
|
|
21
|
+
REGEX_BYTES = 256
|
|
22
|
+
MUTEX = Thread::Mutex.new
|
|
23
|
+
SIGNATURES = {
|
|
24
|
+
setlocale: [[Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP],
|
|
25
|
+
regcomp: [[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT],
|
|
26
|
+
regexec: [[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T,
|
|
27
|
+
Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT],
|
|
28
|
+
regfree: [[Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID],
|
|
29
|
+
strcoll: [[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT]
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
sig { void }
|
|
33
|
+
def initialize
|
|
34
|
+
@functions = T.let(load_functions, T::Hash[Symbol, T.untyped])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { returns(T::Boolean) }
|
|
38
|
+
def available?
|
|
39
|
+
@functions.any?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig do
|
|
43
|
+
params(pattern: String, text: String, settings: T::Array[String],
|
|
44
|
+
fallback: T.proc.returns(T::Boolean)).returns(T::Boolean)
|
|
45
|
+
end
|
|
46
|
+
def match_shell?(pattern, text, settings, &fallback)
|
|
47
|
+
return yield unless available?
|
|
48
|
+
|
|
49
|
+
match?(PosixPattern.new(pattern).source, text, settings)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig do
|
|
53
|
+
params(pattern: String, settings: T::Array[String],
|
|
54
|
+
fallback: T.proc.returns(T::Array[String])).returns(T::Array[String])
|
|
55
|
+
end
|
|
56
|
+
def glob(pattern, settings, &fallback)
|
|
57
|
+
return yield unless available?
|
|
58
|
+
|
|
59
|
+
regex = PosixPattern.new(pattern).source
|
|
60
|
+
matches = candidates(pattern).select { |path| match?(regex, path, settings) }
|
|
61
|
+
matches.sort { |left, right| compare(left, right, settings) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { returns(T::Array[String]) }
|
|
65
|
+
def default_settings
|
|
66
|
+
%w[LC_COLLATE LC_CTYPE].map { |category| locale_value(category) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { params(regex: String, text: String, settings: T::Array[String]).returns(T::Boolean) }
|
|
70
|
+
def match?(regex, text, settings)
|
|
71
|
+
MUTEX.synchronize do
|
|
72
|
+
activate(settings)
|
|
73
|
+
compiled(regex) { |pointer| call(:regexec, pointer, text, 0, nil, 0).zero? }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { params(left: String, right: String, settings: T::Array[String]).returns(Integer) }
|
|
78
|
+
def compare(left, right, settings)
|
|
79
|
+
MUTEX.synchronize do
|
|
80
|
+
activate(settings)
|
|
81
|
+
verdict = call(:strcoll, left, right)
|
|
82
|
+
verdict.zero? ? T.must(left.b <=> right.b) : verdict
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
sig { params(pattern: String).returns(T::Array[String]) }
|
|
89
|
+
def candidates(pattern)
|
|
90
|
+
source = PosixPattern.new(pattern).glob_source
|
|
91
|
+
Dir.glob(source, sort: false)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { params(category: String).returns(String) }
|
|
95
|
+
def locale_value(category)
|
|
96
|
+
values = [ENV.fetch('LC_ALL', nil), ENV.fetch(category, nil), ENV.fetch('LANG', nil)].compact
|
|
97
|
+
values.find { |value| !value.empty? } || 'C'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
101
|
+
def load_functions
|
|
102
|
+
handle = Fiddle::Handle::DEFAULT
|
|
103
|
+
handle['gnu_get_libc_version']
|
|
104
|
+
SIGNATURES.to_h { |name, signature| [name, native_function(handle, name, signature)] }
|
|
105
|
+
rescue Fiddle::DLError
|
|
106
|
+
{}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { params(handle: T.untyped, name: Symbol, signature: T.untyped).returns(T.untyped) }
|
|
110
|
+
def native_function(handle, name, signature)
|
|
111
|
+
arguments, result = signature
|
|
112
|
+
Fiddle::Function.new(handle[name.to_s], arguments, result)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig { params(name: Symbol, args: T.untyped).returns(T.untyped) }
|
|
116
|
+
def call(name, *args)
|
|
117
|
+
@functions.fetch(name).call(*args)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(regex: String, blk: T.proc.params(pointer: T.untyped).returns(T::Boolean)).returns(T::Boolean) }
|
|
121
|
+
def compiled(regex, &blk)
|
|
122
|
+
pointer = Fiddle::Pointer.malloc(REGEX_BYTES, Fiddle::RUBY_FREE)
|
|
123
|
+
return false if call(:regcomp, pointer, regex, REG_EXTENDED).nonzero?
|
|
124
|
+
|
|
125
|
+
free_after(pointer, &blk)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig do
|
|
129
|
+
params(pointer: T.untyped,
|
|
130
|
+
blk: T.proc.params(pointer: T.untyped).returns(T::Boolean)).returns(T::Boolean)
|
|
131
|
+
end
|
|
132
|
+
def free_after(pointer, &blk)
|
|
133
|
+
yield(pointer)
|
|
134
|
+
ensure
|
|
135
|
+
call(:regfree, pointer)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
sig { params(settings: T::Array[String]).void }
|
|
139
|
+
def activate(settings)
|
|
140
|
+
install(LC_COLLATE, settings.fetch(0))
|
|
141
|
+
install(LC_CTYPE, settings.fetch(1))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
sig { params(category: Integer, requested: String).returns(String) }
|
|
145
|
+
def install(category, requested)
|
|
146
|
+
result = call(:setlocale, category, requested)
|
|
147
|
+
return requested unless result.null?
|
|
148
|
+
|
|
149
|
+
call(:setlocale, category, 'C')
|
|
150
|
+
'C'
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
# rubocop:enable Metrics/ClassLength
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class SystemCalls
|
|
6
|
+
# File-test queries for the test/[ builtin (-e -f -d -r -w -x -s -h/-L,
|
|
7
|
+
# the file types -p -b -c -S, the mode bits -g -u, and the fd probe -t),
|
|
8
|
+
# mixed into SystemCalls. Thin File/IO delegations, like the rest of the
|
|
9
|
+
# syscall port.
|
|
10
|
+
module FileTests
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
14
|
+
def exist?(path)
|
|
15
|
+
File.exist?(path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
19
|
+
def file?(path)
|
|
20
|
+
File.file?(path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
24
|
+
def directory?(path)
|
|
25
|
+
File.directory?(path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
29
|
+
def readable?(path)
|
|
30
|
+
File.readable?(path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
34
|
+
def writable?(path)
|
|
35
|
+
# !! because Sorbet's stdlib RBI types File.writable? as T.nilable(Integer)
|
|
36
|
+
# (it returns a real Boolean at runtime, like the other File predicates here).
|
|
37
|
+
!!File.writable?(path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
41
|
+
def executable?(path)
|
|
42
|
+
File.executable?(path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
46
|
+
def file_nonempty?(path)
|
|
47
|
+
File.size?(path).to_i.positive?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
51
|
+
def symlink?(path)
|
|
52
|
+
File.symlink?(path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
56
|
+
def pipe?(path)
|
|
57
|
+
File.pipe?(path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
61
|
+
def blockdev?(path)
|
|
62
|
+
File.blockdev?(path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
66
|
+
def chardev?(path)
|
|
67
|
+
File.chardev?(path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
71
|
+
def socket?(path)
|
|
72
|
+
File.socket?(path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
76
|
+
def setgid?(path)
|
|
77
|
+
File.setgid?(path)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
81
|
+
def setuid?(path)
|
|
82
|
+
File.setuid?(path)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# isatty(fd) for test -t: any unusable descriptor number (closed, huge,
|
|
86
|
+
# negative) is simply not a terminal, as in dash.
|
|
87
|
+
sig { params(fd: Integer).returns(T::Boolean) }
|
|
88
|
+
def tty_fd?(fd)
|
|
89
|
+
IO.new(fd, autoclose: false).tty?
|
|
90
|
+
rescue ArgumentError, RangeError, SystemCallError
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rush
|
|
5
|
+
class SystemCalls
|
|
6
|
+
# Child-process control for the job machinery, mixed into SystemCalls:
|
|
7
|
+
# reaping (JobTable is the sole consumer), process grouping, terminal
|
|
8
|
+
# handover (tcsetpgrp/tcgetpgrp via IO#ioctl), and the platform gate.
|
|
9
|
+
module ProcessControl
|
|
10
|
+
# ioctl request codes for tcgetpgrp/tcsetpgrp, keyed on the build's
|
|
11
|
+
# host_os: Ruby exposes no tc[gs]etpgrp, but IO#ioctl is core and the
|
|
12
|
+
# codes are stable ABI constants — the asm-generic pair on Linux, the
|
|
13
|
+
# sizeof-encoded form on darwin and the BSDs. An unmatched Unix gets
|
|
14
|
+
# no codes: tcgetpgrp answers nil, the terminal is never acquired, and
|
|
15
|
+
# job control degrades to grouping-only (the epic's Fiddle-into-libc
|
|
16
|
+
# fallback was judged not worth a dependency for platforms rush cannot
|
|
17
|
+
# test; revisit if one materialises).
|
|
18
|
+
TIOCGPGRP = { linux: 0x540F, bsd: 0x40047477 }.freeze
|
|
19
|
+
TIOCSPGRP = { linux: 0x5410, bsd: 0x80047476 }.freeze
|
|
20
|
+
TERMINAL_FAMILIES = { /linux/ => :linux, /darwin|bsd|dragonfly/ => :bsd }.freeze
|
|
21
|
+
|
|
22
|
+
# EINTR-style retry: an interactive SIGINT raises Interrupted at a safe
|
|
23
|
+
# point inside the blocking wait; the child is dying from the same signal,
|
|
24
|
+
# so wait again and reap it rather than leaking a zombie.
|
|
25
|
+
def waitpid2(pid)
|
|
26
|
+
Process.waitpid2(pid)
|
|
27
|
+
rescue Interrupted
|
|
28
|
+
retry
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The monitor-mode blocking wait (rush-mv8.4): WUNTRACED also returns a
|
|
32
|
+
# child the terminal (or a kill) has STOPPED, so ^Z hands control back
|
|
33
|
+
# to the shell instead of hanging it — dash waits this way whenever
|
|
34
|
+
# mflag is on, interactive or not (probed: $? = 148 off-tty too).
|
|
35
|
+
def wait_stoppable(pid)
|
|
36
|
+
Process.waitpid2(pid, Process::WUNTRACED)
|
|
37
|
+
rescue Interrupted
|
|
38
|
+
retry
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Non-blocking reap of any finished child: [pid, status], or nil while
|
|
42
|
+
# children exist but none has exited. Raises ECHILD, like waitpid2, when
|
|
43
|
+
# there are no children at all.
|
|
44
|
+
def poll_child
|
|
45
|
+
Process.waitpid2(-1, Process::WNOHANG)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def poll_pid(pid)
|
|
49
|
+
Process.waitpid2(pid, Process::WNOHANG)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The monitor-mode poll (pairs with wait_stoppable as poll_child pairs
|
|
53
|
+
# with waitpid2): WUNTRACED, so the jobs builtin also sees a background
|
|
54
|
+
# job freshly SIGSTOPped since the last wait.
|
|
55
|
+
def poll_stopped
|
|
56
|
+
Process.waitpid2(-1, Process::WNOHANG | Process::WUNTRACED)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def poll_pid_stopped(pid)
|
|
60
|
+
Process.waitpid2(pid, Process::WNOHANG | Process::WUNTRACED)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Place a process into a process group (pid 0 = the caller, pgid 0 = its
|
|
64
|
+
# own pid): the grouping seam. Failure means the double-setpgid race was
|
|
65
|
+
# lost benignly — EACCES: the child already exec'd (its own setpgid won);
|
|
66
|
+
# ESRCH/EPERM: it already exited — so the group is settled either way and
|
|
67
|
+
# the error is swallowed, as dash void-casts the same call.
|
|
68
|
+
# :nocov:
|
|
69
|
+
def setpgid(pid, pgid)
|
|
70
|
+
Process.setpgid(pid, pgid)
|
|
71
|
+
rescue Errno::EACCES, Errno::ESRCH, Errno::EPERM
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Fork a child directly into a process group (pgid 0 = the child's own
|
|
76
|
+
# pid, per POSIX, so the kernel does the leader/joiner defaulting): the
|
|
77
|
+
# double setpgid, issued on both sides of the fork, means whichever side
|
|
78
|
+
# runs first settles the group before the child can exec or the parent
|
|
79
|
+
# can wait — the same dance as dash's forkchild/forkparent. A foreground
|
|
80
|
+
# job leader is also handed the terminal (tty given), child-side before
|
|
81
|
+
# the body runs, exactly where dash's forkchild calls xtcsetpgrp — so
|
|
82
|
+
# the job can never touch the tty before it owns it.
|
|
83
|
+
def fork_grouped(group, tty = nil, &child_main)
|
|
84
|
+
pid = fork { grouped_child(group, tty, &child_main) }
|
|
85
|
+
setpgid(pid, group) if pid
|
|
86
|
+
pid
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def grouped_child(group, tty)
|
|
90
|
+
setpgid(0, group)
|
|
91
|
+
tcsetpgrp(tty, Process.pid) if tty
|
|
92
|
+
yield
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The controlling terminal, for handing between process groups:
|
|
96
|
+
# /dev/tty when the process has one, else a duplicate of the first
|
|
97
|
+
# standard stream that is a tty (dash walks fds 2..0 the same way; the
|
|
98
|
+
# dup means the caller always owns — and closes — its handle, immune
|
|
99
|
+
# to later redirections of the original). nil without a terminal.
|
|
100
|
+
def open_tty
|
|
101
|
+
File.open('/dev/tty', 'r+')
|
|
102
|
+
rescue SystemCallError
|
|
103
|
+
[$stderr, $stdout, $stdin].find(&:tty?)&.dup
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The terminal's foreground process group, or nil when the fd is not a
|
|
107
|
+
# tty or the platform has no known request code: dash's setjobctl uses
|
|
108
|
+
# the same call as the "can we do the tty dance at all" probe.
|
|
109
|
+
def tcgetpgrp(tty)
|
|
110
|
+
family = terminal_family
|
|
111
|
+
read_pgrp(tty, TIOCGPGRP.fetch(family)) if family
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_pgrp(tty, request)
|
|
115
|
+
buffer = [0].pack('l')
|
|
116
|
+
tty.ioctl(request, buffer)
|
|
117
|
+
buffer.unpack1('l').then { |pgid| pgid.is_a?(Integer) ? pgid : nil }
|
|
118
|
+
rescue SystemCallError, IOError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Hand the terminal to a process group. The caller's group often does
|
|
123
|
+
# not own the terminal at that moment (reclaiming after a foreground
|
|
124
|
+
# job): the kernel answers such an ioctl with SIGTTOU, and a caught
|
|
125
|
+
# handler would abort it with EINTR while a default disposition would
|
|
126
|
+
# stop the shell — so the signal is ignored for real (SIG_IGN, not a
|
|
127
|
+
# handler block) for the call's duration, as dash holds TTOU ignored
|
|
128
|
+
# around xtcsetpgrp. Failure (the group already dead, the fd revoked)
|
|
129
|
+
# is swallowed like setpgid's: the reclaim that follows every
|
|
130
|
+
# foreground job settles ownership either way.
|
|
131
|
+
def tcsetpgrp(tty, pgid)
|
|
132
|
+
family = terminal_family
|
|
133
|
+
return unless family
|
|
134
|
+
|
|
135
|
+
ignoring_ttou { tty.ioctl(TIOCSPGRP.fetch(family), [pgid].pack('l')) }
|
|
136
|
+
rescue SystemCallError, IOError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
# :nocov:
|
|
140
|
+
|
|
141
|
+
# Job control needs POSIX process groups, which Windows builds lack (the
|
|
142
|
+
# rush-mv8 platform gate); everywhere else Process.setpgid is real.
|
|
143
|
+
def job_control_supported?
|
|
144
|
+
!RbConfig::CONFIG['host_os'].match?(/mswin|mingw|cygwin/)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Which ioctl vocabulary this build speaks.
|
|
148
|
+
def terminal_family
|
|
149
|
+
host = RbConfig::CONFIG['host_os']
|
|
150
|
+
TERMINAL_FAMILIES.find { |pattern, _family| host.match?(pattern) }&.last
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
# :nocov:
|
|
156
|
+
def ignoring_ttou
|
|
157
|
+
previous = Signal.trap('TTOU', 'IGNORE')
|
|
158
|
+
yield
|
|
159
|
+
ensure
|
|
160
|
+
Signal.trap('TTOU', previous || 'DEFAULT')
|
|
161
|
+
end
|
|
162
|
+
# :nocov:
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|