forked from sds/overcommit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhook_runner.rb
More file actions
213 lines (180 loc) · 6.48 KB
/
hook_runner.rb
File metadata and controls
213 lines (180 loc) · 6.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
module Overcommit
# Responsible for loading the hooks the repository has configured and running
# them, collecting and displaying the results.
class HookRunner # rubocop:disable Metrics/ClassLength
# @param config [Overcommit::Configuration]
# @param logger [Overcommit::Logger]
# @param context [Overcommit::HookContext]
# @param printer [Overcommit::Printer]
def initialize(config, logger, context, printer)
@config = config
@log = logger
@context = context
@printer = printer
@hooks = []
@lock = Mutex.new
@resource = ConditionVariable.new
@slots_available = @config.concurrency
end
# Loads and runs the hooks registered for this {HookRunner}.
def run
# ASSUMPTION: we assume the setup and cleanup calls will never need to be
# interrupted, i.e. they will finish quickly. Should further evidence
# suggest this assumption does not hold, we will have to separately wrap
# these calls to allow some sort of "are you sure?" double-interrupt
# functionality, but until that's deemed necessary let's keep it simple.
InterruptHandler.isolate_from_interrupts do
# Load hooks before setting up the environment so that the repository
# has not been touched yet. This way any load errors at this point don't
# result in Overcommit leaving the repository in a bad state.
load_hooks
# Setup the environment without automatically calling
# `cleanup_environment` on an error. This is because it's possible that
# the `setup_environment` code did not fully complete, so there's no
# guarantee that `cleanup_environment` will be able to accomplish
# anything of value. The safest thing to do is therefore nothing in the
# unlikely case of failure.
@context.setup_environment
begin
run_hooks
ensure
@context.cleanup_environment
end
end
end
private
attr_reader :log
def run_hooks
if @hooks.any?(&:enabled?)
@printer.start_run
# Sort so hooks requiring fewer processors get queued first. This
# ensures we make better use of our available processors
@hooks_left = @hooks.sort_by { |hook| processors_for_hook(hook) }
@threads = Array.new(@config.concurrency) { Thread.new(&method(:consume)) }
begin
InterruptHandler.disable_until_finished_or_interrupted do
@threads.each(&:join)
end
rescue Interrupt
@printer.interrupt_triggered
# We received an interrupt on the main thread, so alert the
# remaining workers that an exception occurred
@interrupted = true
@threads.each { |thread| thread.raise Interrupt }
end
print_results
!(@failed || @interrupted)
else
@printer.nothing_to_run
true # Run was successful
end
end
def consume
loop do
hook = @lock.synchronize { @hooks_left.pop }
break unless hook
run_hook(hook)
end
end
def wait_for_slot(hook)
@lock.synchronize do
slots_needed = processors_for_hook(hook)
loop do
if @slots_available >= slots_needed
@slots_available -= slots_needed
# Give another thread a chance since there are still slots available
@resource.signal if @slots_available > 0
break
elsif @slots_available > 0
# It's possible that another hook that requires fewer slots can be
# served, so give another a chance
@resource.signal
# Wait for a signal from another thread to try again
@resource.wait(@lock)
else
# Otherwise there are not slots left, so just wait for signal
@resource.wait(@lock)
end
end
end
end
def release_slot(hook)
@lock.synchronize do
slots_released = processors_for_hook(hook)
@slots_available += slots_released
if @hooks_left.any?
# Signal once. `wait_for_slot` will perform additional signals if
# there are still slots available. This prevents us from sending out
# useless signals
@resource.signal
end
end
end
def processors_for_hook(hook)
hook.parallelize? ? hook.processors : @config.concurrency
end
def print_results
if @interrupted
@printer.run_interrupted
elsif @failed
@printer.run_failed
elsif @warned
@printer.run_warned
else
@printer.run_succeeded
end
end
def run_hook(hook) # rubocop:disable Metrics/CyclomaticComplexity
status, output = nil, nil
begin
wait_for_slot(hook)
return if should_skip?(hook)
status, output = hook.run_and_transform
rescue Overcommit::Exceptions::MessageProcessingError => ex
status = :fail
output = ex.message
rescue => ex
status = :fail
output = "Hook raised unexpected error\n#{ex.message}\n#{ex.backtrace.join("\n")}"
end
@failed = true if status == :fail
@warned = true if status == :warn
@printer.end_hook(hook, status, output) unless @interrupted
status
rescue Interrupt
@interrupted = true
ensure
release_slot(hook)
end
def should_skip?(hook)
return true if @interrupted || !hook.enabled?
if hook.skip?
if hook.required?
@printer.required_hook_not_skipped(hook)
else
# Tell user if hook was skipped only if it actually would have run
@printer.hook_skipped(hook) if hook.run?
return true
end
end
!hook.run?
end
def load_hooks
require "overcommit/hook/#{@context.hook_type_name}/base"
@hooks += HookLoader::BuiltInHookLoader.new(@config, @context, @log).load_hooks
# Load plugin hooks after so they can subclass existing hooks
@hooks += HookLoader::PluginHookLoader.new(@config, @context, @log).load_hooks
rescue LoadError => ex
# Include a more helpful message that will probably save some confusion
message = 'A load error occurred. ' +
if @config['gemfile']
"Did you forget to specify a gem in your `#{@config['gemfile']}`?"
else
'Did you forget to install a gem?'
end
raise Overcommit::Exceptions::HookLoadError,
"#{message}\n#{ex.message}",
ex.backtrace
end
end
end