ace-assign 0.63.1 → 0.66.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/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
- data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
- data/CHANGELOG.md +62 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +282 -0
- data/docs/usage.md +205 -3
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +257 -0
- data/lib/ace/assign/authority/deployment.rb +627 -0
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +455 -0
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +398 -0
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +598 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +393 -0
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +59 -0
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/private_directory.rb +33 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +76 -0
- data/lib/ace/assign/authority/server.rb +300 -0
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +286 -0
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +187 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +11 -1
- data/lib/ace/assign/cli/commands/delivery.rb +107 -0
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +38 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +56 -4
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +4 -2
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +4 -102
- metadata +186 -8
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "protected_workspace_exclusion"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Molecules
|
|
8
|
+
class LifecycleExclusion
|
|
9
|
+
# Authority-side canonical guards. This owner never selects a Git/cache
|
|
10
|
+
# root, resets a fence, or grants the lifetime workspace writer lease.
|
|
11
|
+
class ControlExclusion
|
|
12
|
+
class Protection < Ace::Runtime::Molecules::ProtectedArtifactSet::Protection
|
|
13
|
+
def initialize(authority_uid:, acl: Authority::PosixAcl.new, **options)
|
|
14
|
+
super(**options)
|
|
15
|
+
@authority_uid, @acl = authority_uid, acl
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def verify!(path, handle, directory:)
|
|
19
|
+
super
|
|
20
|
+
unless (handle.stat.mode & 0o7000).zero? && @acl.entries(path).nil? &&
|
|
21
|
+
(!directory || @acl.entries(path, attribute: "system.posix_acl_default").nil?)
|
|
22
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle control protection differs"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
def trusted_owner?(stat) = [0, @authority_uid].include?(stat.uid)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(authority:, project_id:, descriptor_sha256:, root_identity: nil, files: File, protection: nil)
|
|
31
|
+
unless authority.is_a?(Hash) && project_id.is_a?(String) && token?(project_id) &&
|
|
32
|
+
descriptor_sha256.is_a?(String) && descriptor_sha256.match?(/\A[0-9a-f]{64}\z/) &&
|
|
33
|
+
%w[uid gid].all? { |key| authority[key].is_a?(Integer) && authority[key].positive? } &&
|
|
34
|
+
authority["state_root"].is_a?(String) && authority["state_root"].valid_encoding? &&
|
|
35
|
+
authority["state_root"].bytesize.between?(1, 4096) && !authority["state_root"].include?("\0") &&
|
|
36
|
+
authority["state_root"].start_with?("/") && File.expand_path(authority["state_root"]) == authority["state_root"]
|
|
37
|
+
unavailable!("Lifecycle control selection is malformed")
|
|
38
|
+
end
|
|
39
|
+
@root = File.join(authority.fetch("state_root"), "lifecycle-exclusion", project_id, "control")
|
|
40
|
+
unavailable!("Lifecycle control path is oversized") if @root.bytesize > 4096
|
|
41
|
+
@uid, @gid, @descriptor = authority.values_at("uid", "gid") + [descriptor_sha256.dup.freeze]
|
|
42
|
+
if root_identity
|
|
43
|
+
unless root_identity.is_a?(Hash) && root_identity.keys.sort == %w[device gid inode uid] &&
|
|
44
|
+
root_identity.values.all? { |value| value.is_a?(Integer) && value >= 0 } &&
|
|
45
|
+
root_identity.values_at("uid", "gid") == [@uid, @gid]
|
|
46
|
+
unavailable!("Lifecycle control original identity differs")
|
|
47
|
+
end
|
|
48
|
+
@identity = root_identity.dup.freeze
|
|
49
|
+
end
|
|
50
|
+
@files = files
|
|
51
|
+
@protection = protection || Protection.new(authority_uid: @uid)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def task_key(id) = key!("task", id)
|
|
55
|
+
def assignment_key(id) = key!("assignment", id)
|
|
56
|
+
|
|
57
|
+
def selection!
|
|
58
|
+
with_root do |handles, identities|
|
|
59
|
+
stat = handles.fetch(@root).stat
|
|
60
|
+
@identity ||= {"device" => stat.dev, "inode" => stat.ino, "uid" => stat.uid, "gid" => stat.gid}.freeze
|
|
61
|
+
{"descriptor_sha256" => @descriptor, "root_identity" => @identity}.freeze
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Invoked only after the existing complete registration admission.
|
|
66
|
+
# A partial creation is unavailable, not a missing-fence reset grant.
|
|
67
|
+
def provision_keys!(keys:)
|
|
68
|
+
ordered = keys!(keys)
|
|
69
|
+
with_root do |handles, identities|
|
|
70
|
+
root = handles.fetch(@root)
|
|
71
|
+
ordered.each do |key|
|
|
72
|
+
lock_path, marker_path = paths(key)
|
|
73
|
+
lock, created = create_or_open!(lock_path, "", root)
|
|
74
|
+
begin
|
|
75
|
+
validate_file!(lock_path, lock, empty: true)
|
|
76
|
+
marker = if created
|
|
77
|
+
create_or_open!(marker_path, Atoms::EvidenceDigest.canonical_json(
|
|
78
|
+
{"key" => key, "removed" => false, "removed_at" => nil}), root).first
|
|
79
|
+
else
|
|
80
|
+
@files.open(marker_path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
81
|
+
end
|
|
82
|
+
begin
|
|
83
|
+
live_marker!(key, marker_path, marker)
|
|
84
|
+
ensure
|
|
85
|
+
marker&.close
|
|
86
|
+
end
|
|
87
|
+
ensure
|
|
88
|
+
lock&.close
|
|
89
|
+
end
|
|
90
|
+
unchanged!(handles, identities)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
self
|
|
94
|
+
rescue SystemCallError, IOError, Ace::Runtime::RuntimeUnavailableError
|
|
95
|
+
unavailable!("Lifecycle control provisioning is unavailable")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def with_shared_multi(keys, &block) = with_locks(keys, File::LOCK_SH, &block)
|
|
99
|
+
def with_shared(key, &block) = with_shared_multi([key], &block)
|
|
100
|
+
def with_exclusive(key, deadline: nil, &block) = with_locks([key], File::LOCK_EX, deadline: deadline, &block)
|
|
101
|
+
|
|
102
|
+
# The canonical writer calls this again immediately before CAS. It
|
|
103
|
+
# authenticates the same held keys, not freshly reopened substitutes.
|
|
104
|
+
def verify_unchanged!
|
|
105
|
+
checks = Thread.current[:ace_assign_control_checks]&.fetch(object_id, nil)
|
|
106
|
+
unavailable!("Lifecycle control is not held") unless checks && !checks.empty?
|
|
107
|
+
checks.each(&:call)
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def with_locks(keys, mode, deadline: nil)
|
|
114
|
+
callback_error = nil
|
|
115
|
+
ordered = keys!(keys)
|
|
116
|
+
deadline!(deadline) if deadline
|
|
117
|
+
with_root do |handles, identities|
|
|
118
|
+
locks, markers, marker_facts, lock_facts = [], [], [], []
|
|
119
|
+
begin
|
|
120
|
+
ordered.each do |key|
|
|
121
|
+
path, marker_path = paths(key)
|
|
122
|
+
lock = @files.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
123
|
+
locks << lock
|
|
124
|
+
validate_file!(path, lock, empty: true)
|
|
125
|
+
lock_facts << file_metadata(lock.stat)
|
|
126
|
+
unless lock.flock(mode | File::LOCK_NB)
|
|
127
|
+
raise AttemptErrors::MaintenanceBusy, "Lifecycle control is under maintenance"
|
|
128
|
+
end
|
|
129
|
+
marker = @files.open(marker_path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
130
|
+
markers << marker
|
|
131
|
+
live_marker!(key, marker_path, marker)
|
|
132
|
+
marker_facts << file_metadata(marker.stat)
|
|
133
|
+
end
|
|
134
|
+
deadline!(deadline) if deadline
|
|
135
|
+
unchanged!(handles, identities)
|
|
136
|
+
check = proc do
|
|
137
|
+
ordered.each_with_index do |key, index|
|
|
138
|
+
path, marker_path = paths(key)
|
|
139
|
+
lock, marker = locks.fetch(index), markers.fetch(index)
|
|
140
|
+
validate_file!(path, lock, empty: true)
|
|
141
|
+
unless file_metadata(lock.stat) == lock_facts.fetch(index) &&
|
|
142
|
+
file_metadata(marker.stat) == marker_facts.fetch(index)
|
|
143
|
+
unavailable!("Lifecycle control key changed")
|
|
144
|
+
end
|
|
145
|
+
marker.rewind
|
|
146
|
+
live_marker!(key, marker_path, marker)
|
|
147
|
+
end
|
|
148
|
+
unchanged!(handles, identities)
|
|
149
|
+
end
|
|
150
|
+
registry = Thread.current[:ace_assign_control_checks] ||= {}
|
|
151
|
+
checks = registry[object_id] ||= []
|
|
152
|
+
checks << check
|
|
153
|
+
begin
|
|
154
|
+
begin
|
|
155
|
+
result = yield
|
|
156
|
+
rescue Exception => error
|
|
157
|
+
callback_error = error
|
|
158
|
+
raise
|
|
159
|
+
end
|
|
160
|
+
verify_unchanged!
|
|
161
|
+
ensure
|
|
162
|
+
checks.pop
|
|
163
|
+
registry.delete(object_id) if checks.empty?
|
|
164
|
+
end
|
|
165
|
+
deadline!(deadline) if deadline
|
|
166
|
+
result
|
|
167
|
+
ensure
|
|
168
|
+
close_all!(locks + markers)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
rescue SystemCallError, IOError, Ace::Runtime::RuntimeUnavailableError
|
|
172
|
+
raise if callback_error
|
|
173
|
+
unavailable!("Lifecycle control admission is unavailable")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def with_root
|
|
177
|
+
callback_error = nil
|
|
178
|
+
handles, identities = {}, {}
|
|
179
|
+
begin
|
|
180
|
+
parts = @root.split("/").reject(&:empty?)
|
|
181
|
+
(["/"] + parts.each_index.map { |index| "/" + parts.take(index + 1).join("/") }).each do |path|
|
|
182
|
+
handle = @files.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
183
|
+
handles[path] = handle
|
|
184
|
+
unless handle.stat.directory? && file_identity(handle.stat) == file_identity(@files.lstat(path))
|
|
185
|
+
unavailable!("Lifecycle control directory changed")
|
|
186
|
+
end
|
|
187
|
+
@protection.verify!(path, handle, directory: true)
|
|
188
|
+
identities[path] = file_identity(handle.stat)
|
|
189
|
+
end
|
|
190
|
+
root = handles.fetch(@root).stat
|
|
191
|
+
unless [root.uid, root.gid, root.mode & 0o7777] == [@uid, @gid, 0o700] &&
|
|
192
|
+
(!@identity || [root.dev, root.ino, root.uid, root.gid] == @identity.values_at("device", "inode", "uid", "gid"))
|
|
193
|
+
unavailable!("Lifecycle control original root differs")
|
|
194
|
+
end
|
|
195
|
+
begin
|
|
196
|
+
result = yield handles, identities
|
|
197
|
+
rescue Exception => error
|
|
198
|
+
callback_error = error
|
|
199
|
+
raise
|
|
200
|
+
end
|
|
201
|
+
unchanged!(handles, identities)
|
|
202
|
+
result
|
|
203
|
+
ensure
|
|
204
|
+
close_all!(handles.values)
|
|
205
|
+
end
|
|
206
|
+
rescue SystemCallError, IOError, Ace::Runtime::RuntimeUnavailableError
|
|
207
|
+
raise if callback_error
|
|
208
|
+
unavailable!("Lifecycle control root is unavailable")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def unchanged!(handles, identities)
|
|
212
|
+
handles.each do |path, handle|
|
|
213
|
+
unless file_identity(handle.stat) == identities.fetch(path) &&
|
|
214
|
+
identities.fetch(path) == file_identity(@files.lstat(path))
|
|
215
|
+
unavailable!("Lifecycle control directory changed")
|
|
216
|
+
end
|
|
217
|
+
@protection.verify!(path, handle, directory: true)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def create_or_open!(path, bytes, root)
|
|
222
|
+
created = true
|
|
223
|
+
file = begin
|
|
224
|
+
@files.open(path, File::RDWR | File::CREAT | File::EXCL | File::NOFOLLOW | File::NONBLOCK, 0o600)
|
|
225
|
+
rescue Errno::EEXIST
|
|
226
|
+
created = false
|
|
227
|
+
@files.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
228
|
+
end
|
|
229
|
+
if created
|
|
230
|
+
file.write(bytes)
|
|
231
|
+
file.flush
|
|
232
|
+
file.fsync
|
|
233
|
+
root.fsync
|
|
234
|
+
end
|
|
235
|
+
file.rewind
|
|
236
|
+
[file, created]
|
|
237
|
+
rescue Exception
|
|
238
|
+
file&.close
|
|
239
|
+
raise
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def validate_file!(path, file, empty: false)
|
|
243
|
+
stat = file.stat
|
|
244
|
+
unless stat.file? && stat.nlink == 1 && [stat.uid, stat.gid, stat.mode & 0o7777] == [@uid, @gid, 0o600] &&
|
|
245
|
+
file_identity(stat) == file_identity(@files.lstat(path)) && (empty ? stat.size.zero? : stat.size.between?(1, 8192))
|
|
246
|
+
unavailable!("Lifecycle control file differs")
|
|
247
|
+
end
|
|
248
|
+
@protection.verify!(path, file, directory: false)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def live_marker!(key, path, file)
|
|
252
|
+
validate_file!(path, file)
|
|
253
|
+
before = file.stat
|
|
254
|
+
bytes = file.read(8193)
|
|
255
|
+
state = LifecycleExclusion.workspace_marker!(bytes: bytes, key: key)
|
|
256
|
+
unless bytes.bytesize == before.size && file_metadata(before) == file_metadata(file.stat) &&
|
|
257
|
+
file_metadata(before) == file_metadata(@files.lstat(path))
|
|
258
|
+
unavailable!("Lifecycle control marker changed")
|
|
259
|
+
end
|
|
260
|
+
raise AttemptErrors::Conflict, "Lifecycle control identity is fenced" if state.fetch("removed")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def keys!(keys)
|
|
264
|
+
values = Array(keys).uniq
|
|
265
|
+
unless values.any? && values.all? { |key| key.is_a?(String) && key.match?(/\A(?:task|assignment):[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/) }
|
|
266
|
+
unavailable!("Lifecycle control keys differ")
|
|
267
|
+
end
|
|
268
|
+
values.sort_by { |key| [key.start_with?("task:") ? 0 : 1, key] }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def key!(kind, id)
|
|
272
|
+
unavailable!("Lifecycle control key differs") unless id.is_a?(String) && token?(id)
|
|
273
|
+
"#{kind}:#{id}"
|
|
274
|
+
end
|
|
275
|
+
def token?(value) = value.match?(/\A[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/)
|
|
276
|
+
def paths(key)
|
|
277
|
+
stem = File.join(@root, Digest::SHA256.hexdigest(key))
|
|
278
|
+
[stem + ".lock", stem + ".state.json"]
|
|
279
|
+
end
|
|
280
|
+
def file_identity(stat) = [stat.dev, stat.ino, stat.uid, stat.gid, stat.mode]
|
|
281
|
+
def file_metadata(stat) = file_identity(stat) + [stat.size, stat.mtime, stat.ctime]
|
|
282
|
+
def unavailable!(message) = raise(AttemptErrors::EvidenceUnavailable, message)
|
|
283
|
+
def deadline!(deadline)
|
|
284
|
+
unless deadline.is_a?(Numeric) && deadline.finite? && deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
285
|
+
raise AttemptErrors::MaintenanceBusy, "Lifecycle control admission deadline elapsed"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
def close_all!(files)
|
|
289
|
+
error = nil
|
|
290
|
+
files.reverse_each do |file|
|
|
291
|
+
begin
|
|
292
|
+
file.close unless file.closed?
|
|
293
|
+
rescue SystemCallError, IOError => caught
|
|
294
|
+
error ||= caught
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
raise error if error
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lifecycle_exclusion"
|
|
4
|
+
require_relative "../authority/posix_acl"
|
|
5
|
+
require "ace/runtime/molecules/protected_artifact_set"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Assign
|
|
9
|
+
module Molecules
|
|
10
|
+
class LifecycleExclusion
|
|
11
|
+
# A retained reader lease only. It cannot provision, reset or publish.
|
|
12
|
+
# Source-owned original PreparedInput selects its closed projection.
|
|
13
|
+
class WorkspaceReader
|
|
14
|
+
attr_reader :projection
|
|
15
|
+
class Protection < Ace::Runtime::Molecules::ProtectedArtifactSet::Protection
|
|
16
|
+
def initialize(projection:, acl: Authority::PosixAcl.new, **options)
|
|
17
|
+
super(**options)
|
|
18
|
+
@projection, @acl = projection, acl
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def verify!(path, handle, directory:)
|
|
22
|
+
super
|
|
23
|
+
unless (handle.stat.mode & 0o7000).zero? && @acl.entries(path).nil? &&
|
|
24
|
+
(!directory || @acl.entries(path, attribute: "system.posix_acl_default").nil?)
|
|
25
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle reader protection differs"
|
|
26
|
+
end
|
|
27
|
+
root = @projection.fetch("root_resource")
|
|
28
|
+
if path == root_path(root)
|
|
29
|
+
actual = handle.stat
|
|
30
|
+
unless directory && [actual.dev, actual.ino, actual.uid, actual.gid, actual.mode & 0o7777] ==
|
|
31
|
+
root.values_at("device", "inode", "uid", "gid") + [0o755]
|
|
32
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle reader root identity differs"
|
|
33
|
+
end
|
|
34
|
+
verify_root_mount!(path, handle, root)
|
|
35
|
+
elsif !directory
|
|
36
|
+
unless handle.stat.nlink == 1 && (handle.stat.mode & 0o7777) == 0o644
|
|
37
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle reader file protection differs"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def root_path(root) = root.fetch("view_path")
|
|
45
|
+
|
|
46
|
+
def verify_root_mount!(path, handle, root)
|
|
47
|
+
mount = @mounts.mount_identity(handle)
|
|
48
|
+
table = Ace::Runtime::Molecules::LinuxMountInfo.new(
|
|
49
|
+
@mounts.read("/proc/self/mountinfo", limit: Ace::Runtime::Molecules::LinuxMountInfo::LIMIT))
|
|
50
|
+
selected = table.by_id(mount.fetch("mount_id"))
|
|
51
|
+
unless mount == selected.slice(*mount.keys) && selected.fetch("mountpoint") == path &&
|
|
52
|
+
selected.fetch("filesystem_type") == root.fetch("filesystem_type") &&
|
|
53
|
+
selected.fetch("options").include?("ro") && !selected.fetch("options").include?("rw")
|
|
54
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle reader view is not the selected readonly mount"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def trusted_owner?(stat)
|
|
59
|
+
[0, @projection.fetch("authority_uid")].include?(stat.uid)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(projection:, protection: nil, files: File)
|
|
64
|
+
@projection = JSON.parse(JSON.generate(projection))
|
|
65
|
+
validate_projection!
|
|
66
|
+
pending = [@projection]
|
|
67
|
+
until pending.empty?
|
|
68
|
+
value = pending.pop
|
|
69
|
+
pending.concat(value.values) if value.is_a?(Hash)
|
|
70
|
+
pending.concat(value) if value.is_a?(Array)
|
|
71
|
+
value.freeze
|
|
72
|
+
end
|
|
73
|
+
@root = @projection.fetch("root_resource").fetch("view_path")
|
|
74
|
+
@protection = protection || Protection.new(projection: @projection)
|
|
75
|
+
@files = files
|
|
76
|
+
@handles, @lock, @active = {}, nil, false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def acquire!
|
|
80
|
+
raise AttemptErrors::Conflict, "Lifecycle reader already acquired" if @active || !@handles.empty?
|
|
81
|
+
parts = @root.split("/").reject(&:empty?)
|
|
82
|
+
(["/"] + parts.each_index.map { |index| "/" + parts.take(index + 1).join("/") }).each do |path|
|
|
83
|
+
pin!(path, directory: true)
|
|
84
|
+
end
|
|
85
|
+
@lock = pin!(path_for("lock"), directory: false)
|
|
86
|
+
stat = @lock.stat
|
|
87
|
+
unless stat.size.zero? && [stat.uid, stat.gid] == @projection.values_at("authority_uid", "authority_gid")
|
|
88
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle reader immutable lock differs"
|
|
89
|
+
end
|
|
90
|
+
unless @lock.flock(lock_mode | File::LOCK_NB)
|
|
91
|
+
raise AttemptErrors::MaintenanceBusy, "Lifecycle workspace is under maintenance"
|
|
92
|
+
end
|
|
93
|
+
@active = true
|
|
94
|
+
marker = pin!(path_for("state.json"), directory: false)
|
|
95
|
+
state = read_marker!(marker)
|
|
96
|
+
owner = [marker.stat.uid, marker.stat.gid]
|
|
97
|
+
unless owner == @projection.values_at("authority_uid", "authority_gid") || state.fetch("removed") && owner == [0, 0]
|
|
98
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle marker selected owner differs"
|
|
99
|
+
end
|
|
100
|
+
require_live_marker!(state)
|
|
101
|
+
verify_unchanged!
|
|
102
|
+
self
|
|
103
|
+
rescue Ace::Runtime::RuntimeUnavailableError, SystemCallError, IOError, JSON::ParserError, KeyError, TypeError, ArgumentError
|
|
104
|
+
close!
|
|
105
|
+
raise AttemptErrors::EvidenceUnavailable, "Original lifecycle reader admission is unavailable"
|
|
106
|
+
rescue Exception
|
|
107
|
+
close!
|
|
108
|
+
raise
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def verify_unchanged!
|
|
112
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle reader is not held" unless @active
|
|
113
|
+
@handles.each do |path, entry|
|
|
114
|
+
unless identity(entry.fetch(:handle).stat) == entry.fetch(:identity) &&
|
|
115
|
+
identity(@files.lstat(path)) == entry.fetch(:identity)
|
|
116
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle reader retained object changed"
|
|
117
|
+
end
|
|
118
|
+
@protection.verify!(path, entry.fetch(:handle), directory: entry.fetch(:directory))
|
|
119
|
+
end
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The maintained creator calls this only after confirmed completion.
|
|
124
|
+
# Unknown child ownership retains the lease until its owner exits.
|
|
125
|
+
def close!
|
|
126
|
+
begin
|
|
127
|
+
@lock.flock(File::LOCK_UN) if @active && @lock && !@lock.closed?
|
|
128
|
+
ensure
|
|
129
|
+
close_error = nil
|
|
130
|
+
@handles.values.reverse_each do |entry|
|
|
131
|
+
begin
|
|
132
|
+
entry.fetch(:handle).close unless entry.fetch(:handle).closed?
|
|
133
|
+
rescue IOError, SystemCallError => error
|
|
134
|
+
close_error ||= error
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
@handles.clear
|
|
138
|
+
@active = false
|
|
139
|
+
raise close_error if close_error
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def require_live_marker!(state)
|
|
146
|
+
raise AttemptErrors::Conflict, "Original lifecycle workspace was fenced for removal" if state.fetch("removed")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def lock_mode = File::LOCK_SH
|
|
150
|
+
|
|
151
|
+
def validate_projection!
|
|
152
|
+
fields = %w[authority_gid authority_uid key root_resource worker_cwd_resource]
|
|
153
|
+
resource_fields = %w[device filesystem_type gid host_path inode mount_id uid view_path]
|
|
154
|
+
unless @projection.is_a?(Hash)
|
|
155
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle original reader projection differs"
|
|
156
|
+
end
|
|
157
|
+
root, cwd = @projection.values_at("root_resource", "worker_cwd_resource")
|
|
158
|
+
unless @projection.keys.sort == fields && [root, cwd].all? { |value| value.is_a?(Hash) && value.keys.sort == resource_fields } &&
|
|
159
|
+
%w[authority_uid authority_gid].all? { |key| @projection[key].is_a?(Integer) && @projection[key].positive? }
|
|
160
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle original reader projection differs"
|
|
161
|
+
end
|
|
162
|
+
unless [root, cwd].all? { |value|
|
|
163
|
+
%w[host_path view_path].all? { |key| value[key].is_a?(String) && value[key].valid_encoding? &&
|
|
164
|
+
value[key].bytesize.between?(1, 4096) && !value[key].include?("\0") && value[key].start_with?("/") &&
|
|
165
|
+
File.expand_path(value[key]) == value[key] } &&
|
|
166
|
+
%w[device inode uid gid].all? { |key| value[key].is_a?(Integer) && value[key] >= 0 } &&
|
|
167
|
+
value["mount_id"].is_a?(Integer) && value["mount_id"].positive? &&
|
|
168
|
+
%w[ext4 xfs btrfs tmpfs].include?(value["filesystem_type"]) }
|
|
169
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle original reader resource differs"
|
|
170
|
+
end
|
|
171
|
+
mapping = root.fetch("view_path").delete_prefix("/run/ace/lifecycle-exclusion/")
|
|
172
|
+
digest = Atoms::EvidenceDigest.digest(cwd)
|
|
173
|
+
unless mapping.match?(/\A[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/) &&
|
|
174
|
+
@projection.fetch("key") == "workspace:#{mapping}:#{digest}" &&
|
|
175
|
+
root.fetch("host_path").end_with?("/workspaces/#{mapping}/#{digest}") &&
|
|
176
|
+
root.values_at("uid", "gid") == @projection.values_at("authority_uid", "authority_gid")
|
|
177
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle original reader association differs"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def pin!(path, directory:)
|
|
182
|
+
file = @files.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
183
|
+
@protection.verify!(path, file, directory: directory)
|
|
184
|
+
stat = file.stat
|
|
185
|
+
unless (directory ? stat.directory? : stat.file?) && identity(stat) == identity(@files.lstat(path))
|
|
186
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle reader object is substituted"
|
|
187
|
+
end
|
|
188
|
+
@handles[path] = {handle: file, identity: identity(stat), directory: directory}
|
|
189
|
+
file
|
|
190
|
+
ensure
|
|
191
|
+
file&.close unless @handles.key?(path)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def identity(stat)
|
|
195
|
+
core = [stat.dev, stat.ino, stat.uid, stat.gid, stat.mode]
|
|
196
|
+
stat.directory? ? core : core + [stat.nlink, stat.size, stat.mtime, stat.ctime]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def path_for(suffix)
|
|
200
|
+
File.join(@root, "#{Digest::SHA256.hexdigest(@projection.fetch('key'))}.#{suffix}")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def read_marker!(file)
|
|
204
|
+
size = file.stat.size
|
|
205
|
+
unless size.between?(1, 8192)
|
|
206
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle marker bytes differ"
|
|
207
|
+
end
|
|
208
|
+
bytes = file.read(8193).force_encoding(Encoding::UTF_8)
|
|
209
|
+
unless bytes.valid_encoding? && bytes.bytesize == size
|
|
210
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle marker is not bounded UTF8"
|
|
211
|
+
end
|
|
212
|
+
state = LifecycleExclusion.workspace_marker!(bytes: bytes, key: @projection.fetch("key"))
|
|
213
|
+
@marker_bytes = bytes.freeze
|
|
214
|
+
state
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
# Native activation owns this host lease for the original scope lifetime.
|
|
218
|
+
# An admission deadline must not become a provider lifetime ceiling.
|
|
219
|
+
class WorkspaceNativeReader < WorkspaceReader
|
|
220
|
+
def initialize(projection:, protection: nil, files: File)
|
|
221
|
+
super(projection: projection, protection: protection, files: files)
|
|
222
|
+
@root = @projection.fetch("root_resource").fetch("host_path")
|
|
223
|
+
@protection = protection || WorkspaceHostReader::Protection.new(projection: @projection)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
class WorkspaceHostReader < WorkspaceReader
|
|
228
|
+
class Protection < WorkspaceReader::Protection
|
|
229
|
+
private
|
|
230
|
+
def root_path(root) = root.fetch("host_path")
|
|
231
|
+
def verify_root_mount!(_path, handle, root)
|
|
232
|
+
unless @mounts.mount_identity(handle).fetch("filesystem_type") == root.fetch("filesystem_type")
|
|
233
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle writer filesystem differs"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def initialize(projection:, protection: nil, files: File)
|
|
239
|
+
super(projection: projection, protection: protection, files: files)
|
|
240
|
+
@root = @projection.fetch("root_resource").fetch("host_path")
|
|
241
|
+
@protection = protection || Protection.new(projection: @projection)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def acquire!(deadline:)
|
|
245
|
+
@deadline = deadline
|
|
246
|
+
deadline!(deadline)
|
|
247
|
+
super()
|
|
248
|
+
deadline!(deadline)
|
|
249
|
+
self
|
|
250
|
+
rescue Exception
|
|
251
|
+
close!
|
|
252
|
+
raise
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def verify_unchanged!
|
|
256
|
+
deadline!(@deadline) unless @deadline.nil?
|
|
257
|
+
super
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
def deadline!(value)
|
|
262
|
+
unless value.is_a?(Numeric) && value.finite? && value > Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
263
|
+
raise AttemptErrors::MaintenanceBusy, "Lifecycle maintenance deadline elapsed"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Inspection evidence only: never permits creation or publication.
|
|
269
|
+
class WorkspaceFenceReader < WorkspaceHostReader
|
|
270
|
+
def fence_evidence!
|
|
271
|
+
verify_unchanged!
|
|
272
|
+
bytes = @marker_bytes
|
|
273
|
+
reference = {"path" => path_for("state.json").freeze,
|
|
274
|
+
"sha256" => Digest::SHA256.hexdigest(bytes).freeze, "bytes" => bytes.bytesize}.freeze
|
|
275
|
+
{"reference" => reference, "bytes" => bytes}.freeze
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
def require_live_marker!(_state); end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
class WorkspaceWriter < WorkspaceHostReader
|
|
283
|
+
# Refusal fence only; canonical physical proof is owned separately.
|
|
284
|
+
def publish_removed!(removed_at:)
|
|
285
|
+
verify_unchanged!
|
|
286
|
+
unless removed_at.is_a?(String) && Time.iso8601(removed_at).utc.iso8601 == removed_at
|
|
287
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle fence timestamp differs"
|
|
288
|
+
end
|
|
289
|
+
bytes = Atoms::EvidenceDigest.canonical_json({"key" => @projection.fetch("key"),
|
|
290
|
+
"removed" => true, "removed_at" => removed_at}).freeze
|
|
291
|
+
target = path_for("state.json")
|
|
292
|
+
temporary = File.join(@root, ".#{SecureRandom.hex(16)}.fence")
|
|
293
|
+
begin
|
|
294
|
+
@files.open(temporary, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW | File::NONBLOCK, 0o644) do |file|
|
|
295
|
+
file.chmod(0o644)
|
|
296
|
+
file.write(bytes)
|
|
297
|
+
file.flush
|
|
298
|
+
unless file.stat.file? && file.stat.nlink == 1 && file.stat.size == bytes.bytesize &&
|
|
299
|
+
[file.stat.uid, file.stat.gid, file.stat.mode & 0o7777] == [0, 0, 0o644]
|
|
300
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle temporary fence protection differs"
|
|
301
|
+
end
|
|
302
|
+
file.fsync
|
|
303
|
+
end
|
|
304
|
+
verify_unchanged!
|
|
305
|
+
@files.rename(temporary, target)
|
|
306
|
+
@handles.fetch(@root).fetch(:handle).fsync
|
|
307
|
+
previous = @handles.delete(target)
|
|
308
|
+
previous.fetch(:handle).close
|
|
309
|
+
marker = pin!(target, directory: false)
|
|
310
|
+
unless [marker.stat.uid, marker.stat.gid] == [0, 0] && read_marker!(marker).fetch("removed")
|
|
311
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle published fence owner differs"
|
|
312
|
+
end
|
|
313
|
+
verify_unchanged!
|
|
314
|
+
reference = {"path" => target.freeze, "sha256" => Digest::SHA256.hexdigest(bytes).freeze,
|
|
315
|
+
"bytes" => bytes.bytesize}.freeze
|
|
316
|
+
{"reference" => reference, "bytes" => bytes}.freeze
|
|
317
|
+
ensure
|
|
318
|
+
@files.unlink(temporary) if @files.exist?(temporary)
|
|
319
|
+
end
|
|
320
|
+
rescue SystemCallError, IOError, ArgumentError
|
|
321
|
+
raise AttemptErrors::EvidenceUnavailable, "Lifecycle removal fence publication is unavailable"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
private
|
|
325
|
+
def lock_mode = File::LOCK_EX
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|