Module: Spill::Narrator
- Defined in:
- lib/spill/narrator.rb
Overview
Contract with narrator.swift: the report text goes in on stdin, the summary comes back on stdout, and any nonzero exit (model unavailable, refusal, crash) means "no summary" — the caller stays silent.
Constant Summary collapse
- COMPILE_TIMEOUT_SECONDS =
120
Class Method Summary collapse
- .available? ⇒ Boolean
- .cache_path ⇒ Object
- .clean(str) ⇒ Object
-
.compile(path) ⇒ Object
Compiles to a temp path and renames into place, so a concurrent or interrupted run can never leave a half-written binary at the cache path.
- .compile_command(tmp) ⇒ Object
- .compiled_binary ⇒ Object
- .failure_marker(path) ⇒ Object
-
.kill(wait_thr) ⇒ Object
Bare KILL on purpose: the helper is a one-shot stdin→stdout filter with nothing to clean up, and a wedged model process must not stall the report.
- .narrate(text, timeout: 30, binary: nil) ⇒ Object
- .run(bin, text, timeout) ⇒ Object
- .swift_source ⇒ Object
- .swiftc(tmp, timeout: COMPILE_TIMEOUT_SECONDS) ⇒ Object
- .write_input(stdin, text) ⇒ Object
Class Method Details
.available? ⇒ Boolean
13 14 15 |
# File 'lib/spill/narrator.rb', line 13 def available? RUBY_PLATFORM.include?("darwin") end |
.cache_path ⇒ Object
44 45 46 47 |
# File 'lib/spill/narrator.rb', line 44 def cache_path cache_home = ENV["XDG_CACHE_HOME"] || File.("~/.cache") File.join(cache_home, "spill", "narrator-#{Spill::VERSION}") end |
.clean(str) ⇒ Object
140 141 142 143 144 145 |
# File 'lib/spill/narrator.rb', line 140 def clean(str) return nil if str.nil? stripped = str.strip stripped.empty? ? nil : stripped end |
.compile(path) ⇒ Object
Compiles to a temp path and renames into place, so a concurrent or interrupted run can never leave a half-written binary at the cache path. A failed compile leaves a marker so we don't pay for swiftc on every run; the marker is version-stamped along with the binary, so a new spill version retries.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/spill/narrator.rb', line 62 def compile(path) FileUtils.mkdir_p(File.dirname(path)) tmp = "#{path}.tmp#{Process.pid}" compiled = begin swiftc(tmp) rescue StandardError false end if compiled File.rename(tmp, path) # A marker from a concurrent failed compile must not outlive a # working binary — it would block recompiles after a self-heal. FileUtils.rm_f(failure_marker(path)) true else FileUtils.rm_f(tmp) FileUtils.touch(failure_marker(path)) false end rescue StandardError false end |
.compile_command(tmp) ⇒ Object
105 106 107 |
# File 'lib/spill/narrator.rb', line 105 def compile_command(tmp) [ "swiftc", swift_source, "-o", tmp ] end |
.compiled_binary ⇒ Object
36 37 38 39 40 41 42 |
# File 'lib/spill/narrator.rb', line 36 def compiled_binary path = cache_path return path if File.exist?(path) return nil if File.exist?(failure_marker(path)) compile(path) ? path : nil end |
.failure_marker(path) ⇒ Object
49 50 51 |
# File 'lib/spill/narrator.rb', line 49 def failure_marker(path) "#{path}.failed" end |
.kill(wait_thr) ⇒ Object
Bare KILL on purpose: the helper is a one-shot stdin→stdout filter with nothing to clean up, and a wedged model process must not stall the report.
132 133 134 135 136 137 138 |
# File 'lib/spill/narrator.rb', line 132 def kill(wait_thr) Process.kill("KILL", wait_thr.pid) rescue Errno::ESRCH nil ensure wait_thr.join end |
.narrate(text, timeout: 30, binary: nil) ⇒ Object
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/spill/narrator.rb', line 17 def narrate(text, timeout: 30, binary: nil) bin = binary || compiled_binary return nil if bin.nil? run(bin, text, timeout) rescue SystemCallError # A corrupt cached binary (disk trouble, truncated Mach-O) can fail at # exec and would otherwise fail every run. Drop it — and any stale # failure marker that would block the recompile — so the next run # starts fresh. if bin == cache_path FileUtils.rm_f(cache_path) FileUtils.rm_f(failure_marker(cache_path)) end nil rescue StandardError nil end |
.run(bin, text, timeout) ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/spill/narrator.rb', line 109 def run(bin, text, timeout) stdin, stdout, stderr, wait_thr = Open3.popen3(bin) write_input(stdin, text) if wait_thr.join(timeout) wait_thr.value.success? ? clean(stdout.read) : nil else kill(wait_thr) nil end ensure [ stdin, stdout, stderr ].each { |io| io&.close unless io&.closed? } end |
.swift_source ⇒ Object
53 54 55 |
# File 'lib/spill/narrator.rb', line 53 def swift_source File.join(__dir__, "narrator.swift") end |
.swiftc(tmp, timeout: COMPILE_TIMEOUT_SECONDS) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/spill/narrator.rb', line 85 def swiftc(tmp, timeout: COMPILE_TIMEOUT_SECONDS) pid = Process.spawn(*compile_command(tmp), in: File::NULL, out: File::NULL, err: File::NULL) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout until Process.waitpid(pid, Process::WNOHANG) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline # KILL, not TERM: the compiler holds no state worth a graceful # shutdown, and a hung swiftc must not hang spill. begin Process.kill("KILL", pid) rescue Errno::ESRCH nil end Process.waitpid(pid) return false end sleep 0.05 end Process.last_status.success? end |
.write_input(stdin, text) ⇒ Object
122 123 124 125 126 127 |
# File 'lib/spill/narrator.rb', line 122 def write_input(stdin, text) stdin.write(text) stdin.close rescue IOError, Errno::EPIPE nil end |