cohere-transcribe 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +9 -7
- data/ext/cohere_transcribe_native/.gitignore +6 -0
- data/ext/cohere_transcribe_native/CMakeLists.txt +8 -0
- data/ext/cohere_transcribe_native/audio_abi.cpp +20 -0
- data/ext/cohere_transcribe_native/audio_exports.macos +1 -0
- data/ext/cohere_transcribe_native/audio_exports.map +1 -0
- data/ext/cohere_transcribe_native/test/abi_smoke.rb +26 -0
- data/lib/cohere/transcribe/alignment/aligner.rb +40 -7
- data/lib/cohere/transcribe/asr/native.rb +43 -97
- data/lib/cohere/transcribe/audio/decoder.rb +184 -75
- data/lib/cohere/transcribe/audio/ffmpeg_native.rb +84 -32
- data/lib/cohere/transcribe/audio/segmentation.rb +1 -0
- data/lib/cohere/transcribe/cli.rb +30 -22
- data/lib/cohere/transcribe/configuration.rb +13 -0
- data/lib/cohere/transcribe/constants.rb +1 -1
- data/lib/cohere/transcribe/doctor.rb +2 -1
- data/lib/cohere/transcribe/hub.rb +446 -58
- data/lib/cohere/transcribe/input.rb +18 -3
- data/lib/cohere/transcribe/internal/interruptible_native_call.rb +70 -0
- data/lib/cohere/transcribe/internal/session_ownership.rb +61 -0
- data/lib/cohere/transcribe/internal/utf8.rb +19 -0
- data/lib/cohere/transcribe/model_identity.rb +10 -1
- data/lib/cohere/transcribe/output/publication.rb +95 -86
- data/lib/cohere/transcribe/pytorch_checkpoint.rb +38 -22
- data/lib/cohere/transcribe/runtime/engine.rb +37 -16
- data/lib/cohere/transcribe/runtime/preparation.rb +212 -35
- data/lib/cohere/transcribe/runtime/resources.rb +28 -62
- data/lib/cohere/transcribe/state/checkpoint.rb +3 -2
- data/lib/cohere/transcribe/state/io.rb +217 -117
- data/lib/cohere/transcribe/state/locking.rb +383 -64
- data/lib/cohere/transcribe/state/manifest.rb +21 -22
- data/lib/cohere/transcribe/types.rb +25 -11
- data/lib/cohere/transcribe/version.rb +1 -1
- data/sig/cohere/transcribe.rbs +1 -0
- metadata +5 -1
|
@@ -2,19 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "rubygems/version"
|
|
5
6
|
require "tmpdir"
|
|
6
7
|
|
|
7
8
|
module Cohere
|
|
8
9
|
module Transcribe
|
|
9
10
|
module State
|
|
10
11
|
LOCK_DIRECTORY_PREFIX = "cohere-transcribe"
|
|
12
|
+
LOCK_DIRECTORY_NAME = ".cohere-transcribe-locks"
|
|
11
13
|
LOCK_SUFFIX = ".lock"
|
|
14
|
+
LEGACY_LOCK_REMOVAL_VERSION = "0.2.0"
|
|
12
15
|
|
|
13
|
-
OutputLockTarget = Data.define(:path, :identity)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
OutputLockTarget = Data.define(:path, :identity)
|
|
17
|
+
LockPath = Data.define(:path, :role) do
|
|
18
|
+
def canonical?
|
|
19
|
+
role == :canonical
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def legacy?
|
|
23
|
+
role == :legacy
|
|
16
24
|
end
|
|
17
25
|
end
|
|
26
|
+
private_constant :LockPath
|
|
18
27
|
|
|
19
28
|
class OutputSetLock
|
|
20
29
|
@guard = Mutex.new
|
|
@@ -23,79 +32,173 @@ module Cohere
|
|
|
23
32
|
class << self
|
|
24
33
|
attr_reader :guard, :active
|
|
25
34
|
|
|
26
|
-
def acquire(target, blocking: false, operation_hook: nil)
|
|
35
|
+
def acquire(target, blocking: false, operation_hook: nil, ownership_hook: nil)
|
|
36
|
+
State.ensure_lock_acquisition_allowed!(target)
|
|
37
|
+
completed = false
|
|
38
|
+
failed = false
|
|
27
39
|
guard.synchronize do
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
handles = []
|
|
41
|
+
lock = nil
|
|
42
|
+
lock_paths = State.lock_path_specs_for_target(target)
|
|
43
|
+
paths = lock_paths.map(&:path)
|
|
44
|
+
keys = paths.map(&:to_s)
|
|
45
|
+
if keys.any? { |key| active.key?(key) }
|
|
32
46
|
raise TranscriptionRuntimeError,
|
|
33
47
|
"Another transcription job owns output set #{target.identity}"
|
|
34
48
|
end
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
lock_paths.each_with_index do |lock_path, index|
|
|
51
|
+
path = lock_path.path
|
|
52
|
+
handle = nil
|
|
53
|
+
Thread.handle_interrupt(Object => :never) do
|
|
54
|
+
handle = State.open_lock_file(path, shared: lock_path.canonical?)
|
|
55
|
+
handles << handle
|
|
56
|
+
end
|
|
57
|
+
operation = File::LOCK_EX
|
|
58
|
+
operation |= File::LOCK_NB unless blocking
|
|
59
|
+
unless State.acquire_exclusive_file_lock(handle, operation, target: target, path: path)
|
|
60
|
+
raise TranscriptionRuntimeError,
|
|
61
|
+
"Another transcription process owns output set #{target.identity} (lock #{path})"
|
|
62
|
+
end
|
|
63
|
+
operation_hook&.call(:after_flock, target) if index.zero?
|
|
64
|
+
State.verify_lock_identity!(
|
|
65
|
+
path,
|
|
66
|
+
handle,
|
|
67
|
+
message: "Output lock changed while acquiring #{target.identity}"
|
|
68
|
+
)
|
|
69
|
+
State.refresh_legacy_lock(handle) if lock_path.legacy?
|
|
43
70
|
end
|
|
44
|
-
operation_hook&.call(:after_flock, target)
|
|
45
|
-
State.verify_lock_identity!(
|
|
46
|
-
target.path,
|
|
47
|
-
handle,
|
|
48
|
-
message: "Output lock changed while acquiring #{target.identity}"
|
|
49
|
-
)
|
|
50
71
|
|
|
51
|
-
lock = new(target,
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
lock = new(target, handles, keys, roles: lock_paths.map(&:role))
|
|
73
|
+
keys.each do |key|
|
|
74
|
+
active[key] = lock
|
|
75
|
+
end
|
|
76
|
+
ownership_hook&.call(lock)
|
|
77
|
+
completed = true
|
|
54
78
|
lock
|
|
55
79
|
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
|
|
56
|
-
|
|
80
|
+
failed = true
|
|
81
|
+
raise TranscriptionRuntimeError,
|
|
82
|
+
"Another transcription process owns output set #{target.identity}"
|
|
83
|
+
rescue SystemCallError => e
|
|
84
|
+
failed = true
|
|
57
85
|
raise TranscriptionRuntimeError,
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
"Cannot acquire output lock for #{target.identity}: #{e.message}",
|
|
87
|
+
cause: e
|
|
88
|
+
rescue Exception # rubocop:disable Lint/RescueException -- record only this acquisition's unwind
|
|
89
|
+
failed = true
|
|
61
90
|
raise
|
|
62
91
|
ensure
|
|
63
|
-
|
|
92
|
+
unless completed && !failed && !State.lock_acquisition_unwinding?
|
|
93
|
+
Thread.handle_interrupt(Object => :never) do
|
|
94
|
+
delete_active_keys(lock, keys || [])
|
|
95
|
+
close_lock_handles(handles || [])
|
|
96
|
+
lock&.mark_released!
|
|
97
|
+
end
|
|
98
|
+
end
|
|
64
99
|
end
|
|
65
100
|
end
|
|
66
101
|
|
|
67
102
|
def release_all
|
|
68
|
-
guard.synchronize { active.values.
|
|
103
|
+
guard.synchronize { active.values.uniq }.each(&:release)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def close_lock_handles(handles)
|
|
107
|
+
first_error = nil
|
|
108
|
+
Thread.handle_interrupt(Object => :never) do
|
|
109
|
+
handles.reverse_each do |handle|
|
|
110
|
+
begin
|
|
111
|
+
next if handle.closed?
|
|
112
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
113
|
+
first_error ||= e
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
handle.flock(File::LOCK_UN)
|
|
118
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
119
|
+
first_error ||= e
|
|
120
|
+
end
|
|
121
|
+
begin
|
|
122
|
+
handle.close
|
|
123
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
124
|
+
first_error ||= e
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
first_error
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def delete_active_keys(lock, keys)
|
|
132
|
+
first_error = nil
|
|
133
|
+
keys.reverse_each do |key|
|
|
134
|
+
active.delete(key) if active[key].equal?(lock)
|
|
135
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
136
|
+
first_error ||= e
|
|
137
|
+
end
|
|
138
|
+
first_error
|
|
69
139
|
end
|
|
70
140
|
end
|
|
71
141
|
|
|
72
142
|
attr_reader :target
|
|
73
143
|
|
|
74
|
-
def initialize(target,
|
|
144
|
+
def initialize(target, handles, keys, roles: nil)
|
|
75
145
|
@target = target
|
|
76
|
-
@
|
|
77
|
-
@
|
|
146
|
+
@handles = handles.freeze
|
|
147
|
+
@keys = keys.freeze
|
|
148
|
+
@roles = (roles || Array.new(keys.length, :primary)).freeze
|
|
78
149
|
@released = false
|
|
79
150
|
end
|
|
80
151
|
|
|
81
|
-
def
|
|
152
|
+
def verify!
|
|
82
153
|
self.class.guard.synchronize do
|
|
83
|
-
|
|
154
|
+
if @released
|
|
155
|
+
raise TranscriptionRuntimeError,
|
|
156
|
+
"Output lock was released before committing #{target.identity}"
|
|
157
|
+
end
|
|
84
158
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
159
|
+
canonical_held = @roles.include?(:canonical)
|
|
160
|
+
@keys.zip(@handles, @roles).each do |path, handle, role|
|
|
161
|
+
State.verify_lock_identity!(
|
|
162
|
+
path,
|
|
163
|
+
handle,
|
|
164
|
+
message: "Output lock changed while held for #{target.identity}"
|
|
165
|
+
)
|
|
166
|
+
rescue TranscriptionRuntimeError => e
|
|
167
|
+
# The output-adjacent lock is authoritative. A tmp cleaner may
|
|
168
|
+
# remove the 0.1.x compatibility path; replacement is still fatal.
|
|
169
|
+
raise unless canonical_held && role == :legacy && State.missing_lock_path_error?(e)
|
|
91
170
|
end
|
|
92
171
|
end
|
|
93
172
|
nil
|
|
94
173
|
end
|
|
95
174
|
|
|
175
|
+
def release
|
|
176
|
+
cleanup_error = nil
|
|
177
|
+
Thread.handle_interrupt(Object => :never) do
|
|
178
|
+
self.class.guard.synchronize do
|
|
179
|
+
return if @released
|
|
180
|
+
|
|
181
|
+
begin
|
|
182
|
+
handle_error = self.class.close_lock_handles(@handles)
|
|
183
|
+
registration_error = self.class.delete_active_keys(self, @keys)
|
|
184
|
+
cleanup_error = handle_error || registration_error
|
|
185
|
+
ensure
|
|
186
|
+
@released = true
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
raise cleanup_error if cleanup_error
|
|
191
|
+
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
96
195
|
def released?
|
|
97
196
|
@released
|
|
98
197
|
end
|
|
198
|
+
|
|
199
|
+
def mark_released!
|
|
200
|
+
@released = true
|
|
201
|
+
end
|
|
99
202
|
end
|
|
100
203
|
|
|
101
204
|
module_function
|
|
@@ -103,42 +206,112 @@ module Cohere
|
|
|
103
206
|
def lock_target_for_outputs(output_paths)
|
|
104
207
|
parent, stem = output_parent_and_stem(output_paths)
|
|
105
208
|
identity = parent.join(stem).expand_path.cleanpath.to_s
|
|
106
|
-
digest = Digest::SHA256.hexdigest(identity.downcase)
|
|
107
209
|
OutputLockTarget.new(
|
|
108
|
-
path:
|
|
210
|
+
path: canonical_lock_path(identity).freeze,
|
|
109
211
|
identity: identity.freeze
|
|
110
212
|
)
|
|
111
213
|
end
|
|
112
214
|
|
|
113
215
|
def with_output_lock(target, blocking: false)
|
|
114
|
-
lock =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
216
|
+
lock = nil
|
|
217
|
+
primary_error = nil
|
|
218
|
+
Thread.handle_interrupt(Object => :never) do
|
|
219
|
+
Thread.handle_interrupt(Object => :on_blocking) do
|
|
220
|
+
OutputSetLock.acquire(
|
|
221
|
+
target,
|
|
222
|
+
blocking: blocking,
|
|
223
|
+
ownership_hook: ->(acquired_lock) { lock = acquired_lock }
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
Thread.handle_interrupt(Object => :immediate) { yield lock }
|
|
227
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- preserve protected-work failures
|
|
228
|
+
primary_error = e
|
|
229
|
+
raise
|
|
230
|
+
ensure
|
|
231
|
+
begin
|
|
232
|
+
lock&.release
|
|
233
|
+
rescue Exception # rubocop:disable Lint/RescueException -- preserve protected-work failures
|
|
234
|
+
raise unless primary_error || lock_acquisition_unwinding?
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def lock_paths_for_target(target)
|
|
240
|
+
lock_path_specs_for_target(target).map(&:path).freeze
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def lock_path_specs_for_target(target)
|
|
244
|
+
primary = Pathname(target.path).expand_path.cleanpath
|
|
245
|
+
canonical = canonical_lock_path(target.identity)
|
|
246
|
+
legacy = legacy_temporary_lock_path(target.identity)
|
|
247
|
+
unless primary == canonical
|
|
248
|
+
role = primary == legacy ? :legacy : :custom
|
|
249
|
+
return [LockPath.new(path: primary.freeze, role: role)].freeze
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Remove this temporary-registry compatibility path in 0.2.0. Until
|
|
253
|
+
# then, acquiring both paths keeps released 0.1.2 processes coordinated
|
|
254
|
+
# with output-adjacent locks.
|
|
255
|
+
paths = [LockPath.new(path: primary.freeze, role: :canonical)]
|
|
256
|
+
paths << LockPath.new(path: legacy.freeze, role: :legacy) if legacy_lock_compatibility_enabled? && legacy != primary
|
|
257
|
+
paths.freeze
|
|
118
258
|
end
|
|
119
259
|
|
|
120
|
-
def
|
|
260
|
+
def legacy_lock_compatibility_enabled?(version = VERSION)
|
|
261
|
+
Gem::Version.new(version) < Gem::Version.new(LEGACY_LOCK_REMOVAL_VERSION)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def canonical_lock_path(identity)
|
|
265
|
+
identity = Pathname(identity).expand_path.cleanpath
|
|
266
|
+
lock_registry_directory(identity.dirname).join(lock_filename(identity.to_s))
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def lock_registry_directory(output_parent)
|
|
270
|
+
Pathname(output_parent).expand_path.cleanpath.join(LOCK_DIRECTORY_NAME)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def legacy_temporary_lock_path(identity)
|
|
121
274
|
scope = Process.respond_to?(:uid) ? Process.uid.to_s : Digest::SHA256.hexdigest(Dir.home)[0, 16]
|
|
122
|
-
Pathname(Dir.tmpdir).join("#{LOCK_DIRECTORY_PREFIX}-#{scope}")
|
|
275
|
+
Pathname(Dir.tmpdir).expand_path.join("#{LOCK_DIRECTORY_PREFIX}-#{scope}", lock_filename(identity))
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def lock_filename(identity)
|
|
279
|
+
"#{Digest::SHA256.hexdigest(identity.downcase)}#{LOCK_SUFFIX}"
|
|
123
280
|
end
|
|
124
281
|
|
|
125
|
-
def open_lock_file(path)
|
|
282
|
+
def open_lock_file(path, shared: false)
|
|
126
283
|
path = Pathname(path)
|
|
127
284
|
succeeded = false
|
|
128
|
-
|
|
285
|
+
created = false
|
|
286
|
+
validate_lock_directory!(path.dirname, shared: shared)
|
|
129
287
|
inspect_lock_path!(path)
|
|
130
|
-
flags = File::RDWR
|
|
288
|
+
flags = File::RDWR
|
|
131
289
|
flags |= File::NOFOLLOW if defined?(File::NOFOLLOW)
|
|
132
290
|
flags |= File::CLOEXEC if defined?(File::CLOEXEC)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
291
|
+
mode = shared ? shared_lock_file_mode(path.dirname.lstat) : 0o600
|
|
292
|
+
begin
|
|
293
|
+
descriptor, created = open_or_create_lock_descriptor(path, flags, mode)
|
|
294
|
+
access = "r+"
|
|
295
|
+
rescue Errno::EACCES, Errno::EROFS => e
|
|
296
|
+
raise unless shared
|
|
297
|
+
|
|
298
|
+
handle, access = open_shared_lock_after_write_denial(path, flags, mode, e)
|
|
299
|
+
end
|
|
300
|
+
unless handle
|
|
301
|
+
handle = File.new(descriptor, access, autoclose: true)
|
|
302
|
+
descriptor = nil
|
|
303
|
+
end
|
|
136
304
|
opened = verify_lock_identity!(
|
|
137
305
|
path,
|
|
138
306
|
handle,
|
|
139
307
|
message: "Output lock changed while it was being opened or is not regular"
|
|
140
308
|
)
|
|
141
|
-
|
|
309
|
+
if shared
|
|
310
|
+
owned_writable = access == "r+" && Process.respond_to?(:uid) && opened.uid == Process.uid
|
|
311
|
+
apply_lock_mode_if_supported(handle, mode) if (created || owned_writable) && (opened.mode & 0o777) != mode
|
|
312
|
+
else
|
|
313
|
+
validate_owned_private_file!(opened, path)
|
|
314
|
+
end
|
|
142
315
|
succeeded = true
|
|
143
316
|
handle
|
|
144
317
|
rescue Errno::ELOOP, Errno::EISDIR, Errno::ENXIO => e
|
|
@@ -151,17 +324,37 @@ module Cohere
|
|
|
151
324
|
end
|
|
152
325
|
end
|
|
153
326
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
rescue Errno::
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
327
|
+
def open_or_create_lock_descriptor(path, flags, mode)
|
|
328
|
+
2.times do
|
|
329
|
+
return [::IO.sysopen(path.to_s, flags, mode), false]
|
|
330
|
+
rescue Errno::ENOENT
|
|
331
|
+
begin
|
|
332
|
+
return [::IO.sysopen(path.to_s, flags | File::CREAT | File::EXCL, mode), true]
|
|
333
|
+
rescue Errno::EEXIST
|
|
334
|
+
next
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
raise Errno::ENOENT, path.to_s
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def validate_lock_directory!(path, shared: false)
|
|
342
|
+
if shared
|
|
343
|
+
create_shared_lock_directory(path)
|
|
344
|
+
else
|
|
345
|
+
begin
|
|
346
|
+
FileUtils.mkdir_p(path, mode: 0o700)
|
|
347
|
+
rescue SystemCallError => e
|
|
348
|
+
raise TranscriptionRuntimeError, "Cannot prepare output lock directory #{path}: #{e.message}"
|
|
349
|
+
end
|
|
161
350
|
end
|
|
162
351
|
stat = path.lstat
|
|
163
352
|
raise TranscriptionRuntimeError, "Output lock directory is not a real directory: #{path}" unless stat.directory? && !stat.symlink?
|
|
164
353
|
|
|
354
|
+
if shared
|
|
355
|
+
validate_shared_lock_directory_mode!(path, stat)
|
|
356
|
+
return
|
|
357
|
+
end
|
|
165
358
|
return if Gem.win_platform?
|
|
166
359
|
|
|
167
360
|
if Process.respond_to?(:uid) && stat.uid != Process.uid
|
|
@@ -176,6 +369,132 @@ module Cohere
|
|
|
176
369
|
raise TranscriptionRuntimeError, "Cannot prepare output lock directory #{path}: #{e.message}"
|
|
177
370
|
end
|
|
178
371
|
|
|
372
|
+
def create_shared_lock_directory(path)
|
|
373
|
+
parent = path.dirname.lstat
|
|
374
|
+
unless parent.directory? && !parent.symlink?
|
|
375
|
+
raise TranscriptionRuntimeError, "Output lock parent is not a real directory: #{path.dirname}"
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
mode = (parent.mode & 0o3777) | 0o700
|
|
379
|
+
mode |= 0o1000 if mode.anybits?(0o022)
|
|
380
|
+
begin
|
|
381
|
+
Dir.mkdir(path, mode)
|
|
382
|
+
apply_lock_mode_if_supported(path, mode)
|
|
383
|
+
rescue Errno::EEXIST
|
|
384
|
+
existing = path.lstat
|
|
385
|
+
owned = Process.respond_to?(:uid) && existing.uid == Process.uid
|
|
386
|
+
mode_can_be_updated = !Gem.win_platform? && existing.directory? && owned
|
|
387
|
+
apply_lock_mode_if_supported(path, mode) if mode_can_be_updated && (existing.mode & 0o3777) != mode
|
|
388
|
+
rescue SystemCallError => e
|
|
389
|
+
raise TranscriptionRuntimeError, "Cannot prepare output lock directory #{path}: #{e.message}"
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def validate_shared_lock_directory_mode!(path, stat)
|
|
394
|
+
return if Gem.win_platform? || !Process.respond_to?(:uid)
|
|
395
|
+
return if stat.uid == Process.uid
|
|
396
|
+
return if stat.mode.nobits?(0o022) || stat.mode.anybits?(0o1000)
|
|
397
|
+
|
|
398
|
+
raise TranscriptionRuntimeError,
|
|
399
|
+
"Output lock directory is writable by multiple users but has no sticky bit: #{path}. " \
|
|
400
|
+
"Ask its owner or administrator to add the sticky bit, or remove it when no transcription job is running"
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def shared_lock_file_mode(directory_stat)
|
|
404
|
+
mode = 0o600
|
|
405
|
+
mode |= 0o060 if directory_stat.mode.anybits?(0o020) && directory_stat.mode.anybits?(0o010)
|
|
406
|
+
mode |= 0o006 if directory_stat.mode.anybits?(0o002) && directory_stat.mode.anybits?(0o001)
|
|
407
|
+
mode
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def apply_lock_mode_if_supported(target, mode)
|
|
411
|
+
target.chmod(mode)
|
|
412
|
+
rescue SystemCallError => e
|
|
413
|
+
unsupported = [Errno::EPERM::Errno]
|
|
414
|
+
unsupported << Errno::EROFS::Errno
|
|
415
|
+
unsupported << Errno::EOPNOTSUPP::Errno if defined?(Errno::EOPNOTSUPP)
|
|
416
|
+
unsupported << Errno::ENOTSUP::Errno if defined?(Errno::ENOTSUP)
|
|
417
|
+
raise unless unsupported.include?(e.errno)
|
|
418
|
+
|
|
419
|
+
nil
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def refresh_legacy_lock(handle)
|
|
423
|
+
handle.rewind
|
|
424
|
+
handle.write("#{Process.pid}\n")
|
|
425
|
+
handle.flush
|
|
426
|
+
handle.truncate(handle.pos)
|
|
427
|
+
nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def acquire_exclusive_file_lock(handle, operation, target:, path:)
|
|
431
|
+
handle.flock(operation)
|
|
432
|
+
rescue Errno::EBADF => e
|
|
433
|
+
raise TranscriptionRuntimeError,
|
|
434
|
+
"Cannot acquire an exclusive output lock for #{target.identity}: " \
|
|
435
|
+
"the filesystem requires a writable lock file at #{path}",
|
|
436
|
+
cause: e
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def open_shared_lock_after_write_denial(path, flags, mode, original_error)
|
|
440
|
+
descriptor = ::IO.sysopen(path.to_s, readonly_lock_flags)
|
|
441
|
+
readonly = File.new(descriptor, "r", autoclose: true)
|
|
442
|
+
descriptor = nil
|
|
443
|
+
opened = verify_lock_identity!(
|
|
444
|
+
path,
|
|
445
|
+
readonly,
|
|
446
|
+
message: "Output lock changed while opening it read-only"
|
|
447
|
+
)
|
|
448
|
+
if original_error.is_a?(Errno::EACCES) &&
|
|
449
|
+
Process.respond_to?(:uid) && opened.uid == Process.uid
|
|
450
|
+
apply_lock_mode_if_supported(readonly, mode)
|
|
451
|
+
begin
|
|
452
|
+
descriptor = ::IO.sysopen(path.to_s, flags, mode)
|
|
453
|
+
writable = File.new(descriptor, "r+", autoclose: true)
|
|
454
|
+
descriptor = nil
|
|
455
|
+
readonly.close
|
|
456
|
+
readonly = nil
|
|
457
|
+
handle = writable
|
|
458
|
+
writable = nil
|
|
459
|
+
return [handle, "r+"]
|
|
460
|
+
rescue Errno::EACCES, Errno::EROFS
|
|
461
|
+
nil
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
handle = readonly
|
|
466
|
+
readonly = nil
|
|
467
|
+
[handle, "r"]
|
|
468
|
+
ensure
|
|
469
|
+
readonly&.close unless readonly&.closed?
|
|
470
|
+
writable&.close unless writable&.closed?
|
|
471
|
+
::IO.new(descriptor).close if descriptor
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def readonly_lock_flags
|
|
475
|
+
readonly_flags = File::RDONLY
|
|
476
|
+
readonly_flags |= File::NOFOLLOW if defined?(File::NOFOLLOW)
|
|
477
|
+
readonly_flags |= File::CLOEXEC if defined?(File::CLOEXEC)
|
|
478
|
+
readonly_flags
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def ensure_lock_acquisition_allowed!(target)
|
|
482
|
+
return unless lock_acquisition_unwinding?
|
|
483
|
+
|
|
484
|
+
raise TranscriptionRuntimeError,
|
|
485
|
+
"Cannot acquire output lock for #{target.identity} while the current thread is terminating"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Ruby exposes an in-progress Thread#kill unwind only through #status;
|
|
489
|
+
# pending_interrupt? is already false while ensure blocks are running.
|
|
490
|
+
def lock_acquisition_unwinding?
|
|
491
|
+
Thread.current.status == "aborting"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def missing_lock_path_error?(error)
|
|
495
|
+
error.cause.is_a?(Errno::ENOENT)
|
|
496
|
+
end
|
|
497
|
+
|
|
179
498
|
def inspect_lock_path!(path)
|
|
180
499
|
stat = path.lstat
|
|
181
500
|
return if stat.file? && !stat.symlink?
|
|
@@ -202,7 +521,7 @@ module Cohere
|
|
|
202
521
|
current = Pathname(path).lstat
|
|
203
522
|
return opened if opened.file? && same_file?(opened, current)
|
|
204
523
|
|
|
205
|
-
raise TranscriptionRuntimeError, "#{message}: #{path}"
|
|
524
|
+
raise TranscriptionRuntimeError, "#{message}: #{path}", cause: nil
|
|
206
525
|
rescue SystemCallError => e
|
|
207
526
|
raise TranscriptionRuntimeError, "#{message}: #{path}", cause: e
|
|
208
527
|
end
|
|
@@ -52,8 +52,7 @@ module Cohere
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def verify_published_outputs(source_snapshot:, output_paths:, state_path:, asr_contract_key:,
|
|
55
|
-
render_contract_key:, directory_binding: nil
|
|
56
|
-
guard_bindings: nil)
|
|
55
|
+
render_contract_key:, directory_binding:, guard_bindings: nil)
|
|
57
56
|
payload, reason = decode_state(
|
|
58
57
|
state_path,
|
|
59
58
|
directory_binding: directory_binding,
|
|
@@ -72,11 +71,18 @@ module Cohere
|
|
|
72
71
|
)
|
|
73
72
|
return PublicationVerification.new(verified: false, generation_id: nil, reason: mismatch.freeze) if mismatch
|
|
74
73
|
|
|
74
|
+
verify_publication_bindings!(directory_binding, guard_bindings)
|
|
75
75
|
PublicationVerification.new(
|
|
76
76
|
verified: true,
|
|
77
77
|
generation_id: payload.fetch("generation_id").dup.freeze,
|
|
78
78
|
reason: nil
|
|
79
79
|
)
|
|
80
|
+
rescue PublicationDirectoryChangedError => e
|
|
81
|
+
PublicationVerification.new(
|
|
82
|
+
verified: false,
|
|
83
|
+
generation_id: nil,
|
|
84
|
+
reason: "publication parent changed during verification (#{e.message})".freeze
|
|
85
|
+
)
|
|
80
86
|
rescue EncodingError, SystemCallError, TypeError, ArgumentError => e
|
|
81
87
|
PublicationVerification.new(
|
|
82
88
|
verified: false,
|
|
@@ -86,8 +92,7 @@ module Cohere
|
|
|
86
92
|
end
|
|
87
93
|
|
|
88
94
|
def publication_mismatch_reason(payload, source_snapshot:, output_paths:, asr_contract_key:,
|
|
89
|
-
render_contract_key:, directory_binding: nil
|
|
90
|
-
guard_bindings: nil)
|
|
95
|
+
render_contract_key:, directory_binding:, guard_bindings: nil)
|
|
91
96
|
return "state is #{payload["kind"].inspect}, not published" unless payload["kind"] == "published"
|
|
92
97
|
return "state marker ASR contract does not match" unless payload["asr_contract_key"] == asr_contract_key
|
|
93
98
|
return "state marker render contract does not match" unless payload["render_contract_key"] == render_contract_key
|
|
@@ -105,18 +110,11 @@ module Cohere
|
|
|
105
110
|
return "state marker path for #{format} does not match" unless record.is_a?(Hash) && record["name"] == path.basename.to_s
|
|
106
111
|
|
|
107
112
|
begin
|
|
108
|
-
output_record =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
else
|
|
115
|
-
stat = path.lstat
|
|
116
|
-
return "#{format} output is missing or not regular" unless stat.file? && !stat.symlink?
|
|
117
|
-
|
|
118
|
-
[stat.size, Digest::SHA256.file(path).hexdigest]
|
|
119
|
-
end
|
|
113
|
+
output_record = bound_output_record(
|
|
114
|
+
path,
|
|
115
|
+
directory_binding: directory_binding,
|
|
116
|
+
guard_bindings: guard_bindings
|
|
117
|
+
)
|
|
120
118
|
return "#{format} output changed or is not regular" unless output_record
|
|
121
119
|
|
|
122
120
|
size, sha256 = output_record
|
|
@@ -140,18 +138,14 @@ module Cohere
|
|
|
140
138
|
Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
|
|
141
139
|
handle = bound.open_regular(basename)
|
|
142
140
|
end
|
|
143
|
-
rescue
|
|
141
|
+
rescue PublicationEntryError
|
|
144
142
|
bound.verify!
|
|
145
143
|
return nil
|
|
146
144
|
end
|
|
147
145
|
opened = handle.stat
|
|
148
146
|
digest = Digest::SHA256.new
|
|
149
147
|
digest << handle.read(1024 * 1024) until handle.eof?
|
|
150
|
-
unchanged =
|
|
151
|
-
bound.same_regular_entry?(basename, opened)
|
|
152
|
-
rescue TranscriptionRuntimeError
|
|
153
|
-
false
|
|
154
|
-
end
|
|
148
|
+
unchanged = bound.same_regular_entry?(basename, opened)
|
|
155
149
|
unless unchanged
|
|
156
150
|
bound.verify!
|
|
157
151
|
return nil
|
|
@@ -164,6 +158,11 @@ module Cohere
|
|
|
164
158
|
end
|
|
165
159
|
end
|
|
166
160
|
private_class_method :bound_output_record
|
|
161
|
+
|
|
162
|
+
def verify_publication_bindings!(directory_binding, guard_bindings)
|
|
163
|
+
(Array(guard_bindings).compact + [directory_binding].compact).uniq.each(&:verify!)
|
|
164
|
+
end
|
|
165
|
+
private_class_method :verify_publication_bindings!
|
|
167
166
|
end
|
|
168
167
|
end
|
|
169
168
|
end
|