sidekiq 8.0.10 → 8.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/Changes.md +58 -0
- data/README.md +16 -1
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +11 -8
- data/lib/generators/sidekiq/job_generator.rb +15 -3
- data/lib/sidekiq/api.rb +130 -76
- data/lib/sidekiq/capsule.rb +0 -1
- data/lib/sidekiq/cli.rb +2 -1
- data/lib/sidekiq/client.rb +3 -1
- data/lib/sidekiq/component.rb +3 -0
- data/lib/sidekiq/config.rb +4 -5
- data/lib/sidekiq/job.rb +2 -0
- data/lib/sidekiq/job_retry.rb +7 -3
- data/lib/sidekiq/launcher.rb +5 -5
- data/lib/sidekiq/manager.rb +1 -1
- data/lib/sidekiq/paginator.rb +6 -1
- data/lib/sidekiq/profiler.rb +1 -1
- data/lib/sidekiq/scheduled.rb +6 -7
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -334
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +187 -0
- data/lib/sidekiq/tui/tabs/busy.rb +118 -0
- data/lib/sidekiq/tui/tabs/dead.rb +19 -0
- data/lib/sidekiq/tui/tabs/home.rb +144 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
- data/lib/sidekiq/tui/tabs/queues.rb +95 -0
- data/lib/sidekiq/tui/tabs/retries.rb +19 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +380 -0
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +1 -1
- data/lib/sidekiq/web/application.rb +2 -2
- data/lib/sidekiq/web/config.rb +3 -6
- data/lib/sidekiq/web/helpers.rb +43 -3
- data/lib/sidekiq/web.rb +23 -4
- data/lib/sidekiq.rb +7 -0
- data/sidekiq.gemspec +6 -6
- data/web/assets/javascripts/application.js +1 -1
- data/web/assets/stylesheets/style.css +2 -2
- data/web/locales/ar.yml +1 -1
- data/web/locales/fa.yml +1 -1
- data/web/locales/gd.yml +1 -1
- data/web/locales/he.yml +1 -1
- data/web/locales/pt-BR.yml +1 -1
- data/web/locales/ur.yml +1 -1
- data/web/locales/zh-TW.yml +1 -1
- data/web/views/{_paging.erb → _paging.html.erb} +1 -1
- data/web/views/{busy.erb → busy.html.erb} +1 -1
- data/web/views/{metrics.erb → metrics.html.erb} +3 -2
- metadata +51 -35
- data/lib/sidekiq/web/csrf_protection.rb +0 -183
- /data/web/views/{_footer.erb → _footer.html.erb} +0 -0
- /data/web/views/{_job_info.erb → _job_info.html.erb} +0 -0
- /data/web/views/{_metrics_period_select.erb → _metrics_period_select.html.erb} +0 -0
- /data/web/views/{_nav.erb → _nav.html.erb} +0 -0
- /data/web/views/{_poll_link.erb → _poll_link.html.erb} +0 -0
- /data/web/views/{_summary.erb → _summary.html.erb} +0 -0
- /data/web/views/{dashboard.erb → dashboard.html.erb} +0 -0
- /data/web/views/{dead.erb → dead.html.erb} +0 -0
- /data/web/views/{filtering.erb → filtering.html.erb} +0 -0
- /data/web/views/{layout.erb → layout.html.erb} +0 -0
- /data/web/views/{metrics_for_job.erb → metrics_for_job.html.erb} +0 -0
- /data/web/views/{morgue.erb → morgue.html.erb} +0 -0
- /data/web/views/{profiles.erb → profiles.html.erb} +0 -0
- /data/web/views/{queue.erb → queue.html.erb} +0 -0
- /data/web/views/{queues.erb → queues.html.erb} +0 -0
- /data/web/views/{retries.erb → retries.html.erb} +0 -0
- /data/web/views/{retry.erb → retry.html.erb} +0 -0
- /data/web/views/{scheduled.erb → scheduled.html.erb} +0 -0
- /data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +0 -0
data/lib/sidekiq/tui.rb
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# https://sr.ht/~kerrick/ratatui_ruby/
|
|
2
|
+
# https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/examples/
|
|
3
|
+
gem "ratatui_ruby", ">=1.4.0"
|
|
4
|
+
require "ratatui_ruby"
|
|
5
|
+
|
|
6
|
+
RatatuiRuby.debug_mode! if !!ENV["DEBUG"]
|
|
7
|
+
|
|
8
|
+
require "sidekiq/api"
|
|
9
|
+
require "sidekiq/paginator"
|
|
10
|
+
|
|
11
|
+
require_relative "tui/filtering"
|
|
12
|
+
require_relative "tui/controls"
|
|
13
|
+
require_relative "tui/tabs"
|
|
14
|
+
|
|
15
|
+
module Sidekiq
|
|
16
|
+
class TUI
|
|
17
|
+
include Sidekiq::Component
|
|
18
|
+
|
|
19
|
+
PageOptions = Data.define(:page, :size)
|
|
20
|
+
|
|
21
|
+
REFRESH_INTERVAL_SECONDS = 2
|
|
22
|
+
LOCALE_DIRECTORIES = [File.expand_path("#{File.dirname(__FILE__)}/../../web/locales")]
|
|
23
|
+
|
|
24
|
+
# language is meant to be a locale code, e.g.
|
|
25
|
+
# LANG=en_US.utf-8
|
|
26
|
+
def initialize(cfg, language: ENV["LANG"] || "en")
|
|
27
|
+
@lang = language
|
|
28
|
+
@config = cfg
|
|
29
|
+
@base_style = nil
|
|
30
|
+
@last_refresh = Time.at(0)
|
|
31
|
+
@fps = Array.new(2) { 0 }
|
|
32
|
+
@previous_fps = 0
|
|
33
|
+
@showing = :main
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prepare(tui)
|
|
37
|
+
load_locale
|
|
38
|
+
|
|
39
|
+
@tui = tui
|
|
40
|
+
@highlight_style = @tui.style(fg: :light_red, modifiers: [:underlined])
|
|
41
|
+
@hotkey_style = @tui.style(modifiers: [:bold, :underlined])
|
|
42
|
+
# eager load tabs
|
|
43
|
+
all
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def run_loop
|
|
47
|
+
# Must log to a file, terminal is now controlled by Ratatui
|
|
48
|
+
config.logger = Logger.new("tui.log")
|
|
49
|
+
|
|
50
|
+
loop do
|
|
51
|
+
refresh_data if should_refresh?
|
|
52
|
+
render
|
|
53
|
+
break if handle_input == :quit
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render
|
|
58
|
+
track_fps do
|
|
59
|
+
if @showing == :main
|
|
60
|
+
@tui.draw do |frame|
|
|
61
|
+
main_area, controls_area = @tui.layout_split(
|
|
62
|
+
frame.area,
|
|
63
|
+
direction: :vertical,
|
|
64
|
+
constraints: [
|
|
65
|
+
@tui.constraint_fill(1),
|
|
66
|
+
@tui.constraint_length(5)
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Split main area into tabs and content
|
|
71
|
+
tabs_area, content_area = @tui.layout_split(
|
|
72
|
+
main_area,
|
|
73
|
+
direction: :vertical,
|
|
74
|
+
constraints: [
|
|
75
|
+
@tui.constraint_length(3),
|
|
76
|
+
@tui.constraint_fill(1)
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
all_tabs = all
|
|
81
|
+
tabs = @tui.tabs(
|
|
82
|
+
titles: all_tabs.map { |tab| t(tab.name) },
|
|
83
|
+
selected_index: all_tabs.index(current_tab),
|
|
84
|
+
block: @tui.block(title: " #{Sidekiq::NAME}", borders: [:all], title_style: @tui.style(fg: :light_red, modifiers: [:bold])),
|
|
85
|
+
divider: " | ",
|
|
86
|
+
highlight_style: @highlight_style,
|
|
87
|
+
style: @base_style
|
|
88
|
+
)
|
|
89
|
+
frame.render_widget(tabs, tabs_area)
|
|
90
|
+
|
|
91
|
+
render_content_area(frame, content_area)
|
|
92
|
+
render_controls(frame, controls_area)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if @showing == :help
|
|
97
|
+
@tui.draw do |frame|
|
|
98
|
+
main_area, controls_area = @tui.layout_split(
|
|
99
|
+
frame.area,
|
|
100
|
+
direction: :vertical,
|
|
101
|
+
constraints: [
|
|
102
|
+
@tui.constraint_fill(1),
|
|
103
|
+
@tui.constraint_length(4)
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
content = @tui.block(
|
|
107
|
+
title: " #{Sidekiq::NAME} ",
|
|
108
|
+
borders: [:all],
|
|
109
|
+
title_style: @tui.style(fg: :light_red, modifiers: [:bold]),
|
|
110
|
+
children: [
|
|
111
|
+
# TODO convert to table
|
|
112
|
+
@tui.paragraph(
|
|
113
|
+
text: [
|
|
114
|
+
@tui.text_line(spans: ["Welcome to the Sidekiq Terminal UI"], alignment: :center),
|
|
115
|
+
@tui.text_line(spans: [
|
|
116
|
+
@tui.text_span(content: "Global hotkeys")
|
|
117
|
+
]),
|
|
118
|
+
@tui.text_line(spans: []),
|
|
119
|
+
@tui.text_line(spans: [
|
|
120
|
+
@tui.text_span(content: "Esc", style: @hotkey_style),
|
|
121
|
+
@tui.text_span(content: ": Close this window")
|
|
122
|
+
]),
|
|
123
|
+
@tui.text_line(spans: [
|
|
124
|
+
@tui.text_span(content: "←/→", style: @hotkey_style),
|
|
125
|
+
@tui.text_span(content: ": Move between tabs")
|
|
126
|
+
]),
|
|
127
|
+
@tui.text_line(spans: [
|
|
128
|
+
@tui.text_span(content: "h/l", style: @hotkey_style),
|
|
129
|
+
@tui.text_span(content: ": Move to prev/next page of data")
|
|
130
|
+
]),
|
|
131
|
+
@tui.text_line(spans: [
|
|
132
|
+
@tui.text_span(content: "j/k", style: @hotkey_style),
|
|
133
|
+
@tui.text_span(content: ": Move to prev/next row in current page")
|
|
134
|
+
]),
|
|
135
|
+
@tui.text_line(spans: [
|
|
136
|
+
@tui.text_span(content: "x", style: @hotkey_style),
|
|
137
|
+
@tui.text_span(content: ": Select/deselect current row")
|
|
138
|
+
]),
|
|
139
|
+
@tui.text_line(spans: [
|
|
140
|
+
@tui.text_span(content: "A", style: @hotkey_style),
|
|
141
|
+
@tui.text_span(content: ": Select/deselect All rows in current page")
|
|
142
|
+
]),
|
|
143
|
+
@tui.text_line(spans: [
|
|
144
|
+
@tui.text_span(content: "q", style: @hotkey_style),
|
|
145
|
+
@tui.text_span(content: ": Quit")
|
|
146
|
+
])
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
frame.render_widget(content, main_area)
|
|
152
|
+
controls = @tui.block(
|
|
153
|
+
title: t("Controls"),
|
|
154
|
+
borders: [:all],
|
|
155
|
+
children: [
|
|
156
|
+
@tui.paragraph(
|
|
157
|
+
text: [
|
|
158
|
+
@tui.text_line(spans: [
|
|
159
|
+
@tui.text_span(content: "Esc", style: @hotkey_style),
|
|
160
|
+
@tui.text_span(content: ": Close ")
|
|
161
|
+
])
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
frame.render_widget(controls, controls_area)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def render_content_area(frame, content_area)
|
|
173
|
+
return render_error(frame, content_area, current_tab.error) if current_tab.error
|
|
174
|
+
|
|
175
|
+
current_tab.render(@tui, frame, content_area)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def render_controls(frame, area)
|
|
179
|
+
active_keys = current_tab.controls.filter { |hash| hash[:description] }
|
|
180
|
+
|
|
181
|
+
# Split controls into two lines, 8 is arbitrary
|
|
182
|
+
# TODO Dynamically split based on term width?
|
|
183
|
+
first = active_keys[...8]
|
|
184
|
+
lines = []
|
|
185
|
+
lines << @tui.text_line(spans: first.map { |hash|
|
|
186
|
+
[
|
|
187
|
+
@tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
|
|
188
|
+
@tui.text_span(content: ": #{t(hash[:description])} ")
|
|
189
|
+
]
|
|
190
|
+
}.flatten)
|
|
191
|
+
|
|
192
|
+
last = active_keys[8...]
|
|
193
|
+
lines << if last && last.size > 0
|
|
194
|
+
@tui.text_line(spans: last.map { |hash|
|
|
195
|
+
[
|
|
196
|
+
@tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
|
|
197
|
+
@tui.text_span(content: ": #{t(hash[:description])} ")
|
|
198
|
+
]
|
|
199
|
+
}.flatten)
|
|
200
|
+
else
|
|
201
|
+
@tui.text_line(spans: [])
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
footer = [
|
|
205
|
+
@tui.text_span(content: "Redis: #{redis_url} "),
|
|
206
|
+
@tui.text_span(content: "#{t("Now")}: #{Time.now.utc} "),
|
|
207
|
+
@tui.text_span(content: "#{t("Locale")}: #{@lang}")
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
if current_tab.data[:filter]
|
|
211
|
+
@filter_style = @tui.style(fg: :white, bg: :dark_gray)
|
|
212
|
+
footer += [
|
|
213
|
+
@tui.text_span(content: " #{t("Filter")}: ", style: @filter_style),
|
|
214
|
+
@tui.text_span(content: current_tab.data[:filter], style: @filter_style),
|
|
215
|
+
@tui.text_span(content: "_", style: @tui.style(fg: :white, bg: :dark_gray, modifiers: [:slow_blink]))
|
|
216
|
+
]
|
|
217
|
+
end
|
|
218
|
+
footer << @tui.text_span(content: " FPS: #{previous_fps}") if debugging?
|
|
219
|
+
lines << @tui.text_line(spans: footer)
|
|
220
|
+
|
|
221
|
+
controls = @tui.block(title: t("Controls"), borders: [:all],
|
|
222
|
+
children: [@tui.paragraph(text: lines)])
|
|
223
|
+
frame.render_widget(controls, area)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def handle_input
|
|
227
|
+
# We shouldn't need more than 10 FPS for a data-oriented app.
|
|
228
|
+
# This throttles down our CPU usage. Default is 60 FPS.
|
|
229
|
+
case @tui.poll_event(timeout: 0.1)
|
|
230
|
+
in {type: :key, code: "esc"} if @showing == :help
|
|
231
|
+
@showing = :main
|
|
232
|
+
in {type: :key, code: code} if current_tab.filtering? && code.length == 1
|
|
233
|
+
current_tab.append_to_filter(code)
|
|
234
|
+
current_tab.refresh_data
|
|
235
|
+
in {type: :key, code:, modifiers:}
|
|
236
|
+
control = current_tab.controls.find { |ctrl|
|
|
237
|
+
ctrl[:code] == code &&
|
|
238
|
+
(ctrl[:modifiers] || []) == (modifiers || [])
|
|
239
|
+
}
|
|
240
|
+
return unless control
|
|
241
|
+
control[:action].call(self, current_tab).tap {
|
|
242
|
+
refresh_data if control[:refresh]
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
# Ignore other events
|
|
246
|
+
end
|
|
247
|
+
rescue => ex
|
|
248
|
+
logger.error { [ex.message, ex.backtrace] }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def redis_url
|
|
252
|
+
Sidekiq.redis do |conn|
|
|
253
|
+
conn.config.server_url
|
|
254
|
+
end
|
|
255
|
+
rescue
|
|
256
|
+
"N/A"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def should_refresh?
|
|
260
|
+
Time.now - @last_refresh >= REFRESH_INTERVAL_SECONDS
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def refresh_data
|
|
264
|
+
# logger.info GC.stat
|
|
265
|
+
current_tab.refresh_data
|
|
266
|
+
@last_refresh = Time.now
|
|
267
|
+
rescue => e
|
|
268
|
+
handle_exception(e)
|
|
269
|
+
current_tab.error = e
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def render_error(frame, area, err)
|
|
273
|
+
header = [@tui.text_line(
|
|
274
|
+
spans: [@tui.text_span(content: err.message, style: @tui.style(modifiers: [:bold]))],
|
|
275
|
+
alignment: :center
|
|
276
|
+
)]
|
|
277
|
+
lines = Array(err.backtrace).map { |line| @tui.text_line(spans: [@tui.text_span(content: line)]) }
|
|
278
|
+
|
|
279
|
+
frame.render_widget(
|
|
280
|
+
@tui.paragraph(
|
|
281
|
+
text: header + lines,
|
|
282
|
+
alignment: :left,
|
|
283
|
+
block: @tui.block(title: t("Error"), borders: [:all], border_style: @tui.style(fg: :light_red))
|
|
284
|
+
),
|
|
285
|
+
area
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def show_help
|
|
290
|
+
@showing = :help
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def all
|
|
294
|
+
@all ||= Tabs::All.map { |kls| kls.new(self) }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def current_tab
|
|
298
|
+
@current ||= @all.first
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Navigate tabs to the left or right.
|
|
302
|
+
# @param direction [Symbol] :left or :right
|
|
303
|
+
def navigate(direction)
|
|
304
|
+
index_change = (direction == :right) ? 1 : -1
|
|
305
|
+
@current = @all[(@all.index(current_tab) + index_change) % @all.size]
|
|
306
|
+
@current.reset_data
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
public def t(msg, options = nil)
|
|
310
|
+
string = @strings[msg] || msg
|
|
311
|
+
if options.nil?
|
|
312
|
+
string
|
|
313
|
+
else
|
|
314
|
+
string % options
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def load_strings(lang)
|
|
319
|
+
{}.tap do |all|
|
|
320
|
+
find_locale_files(lang).each do |file|
|
|
321
|
+
strs = YAML.safe_load_file(file)
|
|
322
|
+
all.merge! strs[lang]
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def locale_files
|
|
328
|
+
@@locale_files ||= LOCALE_DIRECTORIES.flat_map { |path|
|
|
329
|
+
Dir["#{path}/*.yml"]
|
|
330
|
+
}
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def available_locales
|
|
334
|
+
@@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def find_locale_files(lang)
|
|
338
|
+
locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def load_locale
|
|
342
|
+
require "yaml"
|
|
343
|
+
lang = @lang.split(".").first # "en_US"
|
|
344
|
+
while lang.size > 0
|
|
345
|
+
hash = load_strings(lang)
|
|
346
|
+
if hash.size > 0
|
|
347
|
+
# found a working language dataset
|
|
348
|
+
@lang = lang
|
|
349
|
+
@strings = hash
|
|
350
|
+
Sidekiq.logger.debug { "using the #{lang} locale" }
|
|
351
|
+
break
|
|
352
|
+
end
|
|
353
|
+
# Try "en_US", "en_U", "en_", "en"
|
|
354
|
+
# It's ugly and bruteforce but it works
|
|
355
|
+
lang = lang[..-2]
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def track_fps
|
|
360
|
+
# We hold two fps buckets: one for current second, one for previous second
|
|
361
|
+
idx = Time.now.to_i % 2
|
|
362
|
+
@fps[idx] += 1
|
|
363
|
+
yield
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def previous_fps
|
|
367
|
+
curidx = Time.now.to_i % 2
|
|
368
|
+
prev = (curidx == 1) ? 0 : 1
|
|
369
|
+
if (val = @fps[prev]) != 0
|
|
370
|
+
@previous_fps = val
|
|
371
|
+
@fps[prev] = 0
|
|
372
|
+
end
|
|
373
|
+
@previous_fps
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def debugging?
|
|
377
|
+
!!ENV["DEBUG"]
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
|
@@ -117,7 +117,7 @@ module Sidekiq
|
|
|
117
117
|
if content.is_a? Symbol
|
|
118
118
|
unless respond_to?(:"_erb_#{content}")
|
|
119
119
|
views = options[:views] || Web.views
|
|
120
|
-
filename = "#{views}/#{content}.erb"
|
|
120
|
+
filename = "#{views}/#{content}.html.erb"
|
|
121
121
|
src = ERB.new(File.read(filename)).src
|
|
122
122
|
|
|
123
123
|
# Need to use lineno less by 1 because erb generates a
|
|
@@ -99,8 +99,8 @@ module Sidekiq
|
|
|
99
99
|
if url_params("identity")
|
|
100
100
|
pro = Sidekiq::ProcessSet[url_params("identity")]
|
|
101
101
|
|
|
102
|
-
pro
|
|
103
|
-
pro
|
|
102
|
+
pro&.quiet! if url_params("quiet")
|
|
103
|
+
pro&.stop! if url_params("stop")
|
|
104
104
|
else
|
|
105
105
|
processes.each do |pro|
|
|
106
106
|
next if pro.embedded?
|
data/lib/sidekiq/web/config.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "sidekiq/web/csrf_protection"
|
|
4
|
-
|
|
5
3
|
module Sidekiq
|
|
6
4
|
class Web
|
|
7
5
|
##
|
|
@@ -24,10 +22,7 @@ module Sidekiq
|
|
|
24
22
|
# and very difficult for us to vendor or provide ourselves. If you are worried
|
|
25
23
|
# about data security and wish to self-host, you can change these URLs.
|
|
26
24
|
profile_view_url: "https://profiler.firefox.com/public/%s",
|
|
27
|
-
profile_store_url: "https://api.profiler.firefox.com/compressed-store"
|
|
28
|
-
# TODO Will be false in Sidekiq 9.0.
|
|
29
|
-
# CSRF is unnecessary if you are using SameSite=(Strict|Lax) cookies.
|
|
30
|
-
csrf: true
|
|
25
|
+
profile_store_url: "https://api.profiler.firefox.com/compressed-store"
|
|
31
26
|
}
|
|
32
27
|
|
|
33
28
|
##
|
|
@@ -54,11 +49,13 @@ module Sidekiq
|
|
|
54
49
|
|
|
55
50
|
# Adds the "Back to App" link in the header
|
|
56
51
|
attr_accessor :app_url
|
|
52
|
+
attr_accessor :assets_path
|
|
57
53
|
|
|
58
54
|
def initialize
|
|
59
55
|
@options = OPTIONS.dup
|
|
60
56
|
@locales = LOCALES
|
|
61
57
|
@views = VIEWS
|
|
58
|
+
@assets_path = ASSETS
|
|
62
59
|
@tabs = DEFAULT_TABS.dup
|
|
63
60
|
@middlewares = []
|
|
64
61
|
@custom_job_info_rows = []
|
data/lib/sidekiq/web/helpers.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
|
-
require "yaml"
|
|
5
4
|
require "cgi/escape"
|
|
6
5
|
|
|
7
6
|
module Sidekiq
|
|
@@ -73,12 +72,42 @@ module Sidekiq
|
|
|
73
72
|
# so extensions can be localized
|
|
74
73
|
@@strings[lang] ||= config.locales.each_with_object({}) do |path, global|
|
|
75
74
|
find_locale_files(lang).each do |file|
|
|
76
|
-
strs =
|
|
75
|
+
strs = parse_yaml_new(file)
|
|
77
76
|
global.merge!(strs[lang])
|
|
78
77
|
end
|
|
79
78
|
end
|
|
80
79
|
end
|
|
81
80
|
|
|
81
|
+
def parse_yaml_old(path)
|
|
82
|
+
require "yaml"
|
|
83
|
+
YAML.safe_load_file(path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def parse_yaml_new(path)
|
|
87
|
+
locale = nil
|
|
88
|
+
map = {}
|
|
89
|
+
IO.readlines(path, chomp: true).each do |line|
|
|
90
|
+
case line
|
|
91
|
+
when /\A\s*\#.*/
|
|
92
|
+
# line comment
|
|
93
|
+
when !locale && /\A([a-zA-Z\-_]+):/
|
|
94
|
+
locale = $1
|
|
95
|
+
map[locale] = {}
|
|
96
|
+
when /\A\s+(\w+):\s+(.+)\z/
|
|
97
|
+
# A few values have double quotes to include special characters in YAML.
|
|
98
|
+
# Strip them off manually as our greedy match will include them.
|
|
99
|
+
key = $1
|
|
100
|
+
s = $2
|
|
101
|
+
s = s[1..] if s[0] == "\""
|
|
102
|
+
s = s[0..-2] if s[-1] == "\""
|
|
103
|
+
map[locale][key] = s
|
|
104
|
+
else
|
|
105
|
+
raise ArgumentError, "unable to parse #{path}: #{line}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
map
|
|
109
|
+
end
|
|
110
|
+
|
|
82
111
|
def to_json(x)
|
|
83
112
|
Sidekiq.dump_json(x)
|
|
84
113
|
end
|
|
@@ -287,6 +316,17 @@ module Sidekiq
|
|
|
287
316
|
%(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
|
|
288
317
|
end
|
|
289
318
|
|
|
319
|
+
def queue_names_by_capsule(pro)
|
|
320
|
+
cap = pro.capsules
|
|
321
|
+
if cap
|
|
322
|
+
cap.map { |k, v| v["weights"].keys.join(", ") }.join("; ")
|
|
323
|
+
else
|
|
324
|
+
# DEPRECATED Backwards compatibility with older processes.
|
|
325
|
+
# 'capsules' element added in v8.0.9
|
|
326
|
+
pro.queues.join(", ")
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
290
330
|
def job_params(job, score)
|
|
291
331
|
"#{score}-#{job["jid"]}"
|
|
292
332
|
end
|
|
@@ -329,7 +369,7 @@ module Sidekiq
|
|
|
329
369
|
end
|
|
330
370
|
|
|
331
371
|
def csrf_tag
|
|
332
|
-
"
|
|
372
|
+
""
|
|
333
373
|
end
|
|
334
374
|
|
|
335
375
|
def csp_nonce
|
data/lib/sidekiq/web.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Sidekiq
|
|
|
13
13
|
ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../web")
|
|
14
14
|
VIEWS = "#{ROOT}/views"
|
|
15
15
|
LOCALES = ["#{ROOT}/locales"]
|
|
16
|
-
LAYOUT = "#{VIEWS}/layout.erb"
|
|
16
|
+
LAYOUT = "#{VIEWS}/layout.html.erb"
|
|
17
17
|
ASSETS = "#{ROOT}/assets"
|
|
18
18
|
|
|
19
19
|
DEFAULT_TABS = {
|
|
@@ -42,6 +42,12 @@ module Sidekiq
|
|
|
42
42
|
@@config.app_url = url
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
def assets_path=(path)
|
|
46
|
+
@@config.assets_path = path
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def assets_path = @@config.assets_path
|
|
50
|
+
|
|
45
51
|
def tabs = @@config.tabs
|
|
46
52
|
|
|
47
53
|
def locales = @@config.locales
|
|
@@ -87,13 +93,27 @@ module Sidekiq
|
|
|
87
93
|
env[:web_config] = Sidekiq::Web.configure
|
|
88
94
|
env[:csp_nonce] = SecureRandom.hex(8)
|
|
89
95
|
env[:redis_pool] = self.class.redis_pool
|
|
90
|
-
app.call(env)
|
|
96
|
+
safe_request?(env) ? app.call(env) : deny(env)
|
|
91
97
|
end
|
|
92
98
|
|
|
93
99
|
def app
|
|
94
100
|
@app ||= build(@@config)
|
|
95
101
|
end
|
|
96
102
|
|
|
103
|
+
def safe_methods?(env)
|
|
104
|
+
%w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def safe_request?(env)
|
|
108
|
+
return true if safe_methods?(env)
|
|
109
|
+
env["HTTP_SEC_FETCH_SITE"] == "same-origin"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def deny(env)
|
|
113
|
+
Sidekiq.logger.warn "attack prevented by #{self.class}"
|
|
114
|
+
[403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
|
|
115
|
+
end
|
|
116
|
+
|
|
97
117
|
private
|
|
98
118
|
|
|
99
119
|
def build(cfg)
|
|
@@ -105,11 +125,10 @@ module Sidekiq
|
|
|
105
125
|
|
|
106
126
|
::Rack::Builder.new do
|
|
107
127
|
use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"],
|
|
108
|
-
root:
|
|
128
|
+
root: cfg.assets_path,
|
|
109
129
|
cascade: true,
|
|
110
130
|
header_rules: rules
|
|
111
131
|
m.each { |middleware, block| use(*middleware, &block) }
|
|
112
|
-
use CsrfProtection if cfg[:csrf]
|
|
113
132
|
run Sidekiq::Web::Application.new(self.class)
|
|
114
133
|
end
|
|
115
134
|
end
|
data/lib/sidekiq.rb
CHANGED
|
@@ -47,6 +47,13 @@ module Sidekiq
|
|
|
47
47
|
puts "Take a deep breath and count to ten..."
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def self.testing!(mode = :fake, &block)
|
|
51
|
+
raise "Unknown testing mode: #{mode}" unless %i[fake disable inline].include?(mode)
|
|
52
|
+
|
|
53
|
+
require "sidekiq/test_api"
|
|
54
|
+
Sidekiq::Testing.__set_test_mode(mode, &block)
|
|
55
|
+
end
|
|
56
|
+
|
|
50
57
|
def self.server?
|
|
51
58
|
defined?(Sidekiq::CLI)
|
|
52
59
|
end
|
data/sidekiq.gemspec
CHANGED
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |gem|
|
|
|
8
8
|
gem.homepage = "https://sidekiq.org"
|
|
9
9
|
gem.license = "LGPL-3.0"
|
|
10
10
|
|
|
11
|
-
gem.executables = ["sidekiq", "sidekiqmon"]
|
|
11
|
+
gem.executables = ["sidekiq", "sidekiqmon", "kiq"]
|
|
12
12
|
gem.files = %w[sidekiq.gemspec README.md Changes.md LICENSE.txt] + `git ls-files | grep -E '^(bin|lib|web)'`.split("\n")
|
|
13
13
|
gem.name = "sidekiq"
|
|
14
14
|
gem.version = Sidekiq::VERSION
|
|
@@ -23,9 +23,9 @@ Gem::Specification.new do |gem|
|
|
|
23
23
|
"rubygems_mfa_required" => "true"
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
gem.add_dependency "redis-client", ">= 0.
|
|
27
|
-
gem.add_dependency "connection_pool", ">=
|
|
28
|
-
gem.add_dependency "rack", ">= 3.
|
|
29
|
-
gem.add_dependency "json", ">= 2.
|
|
30
|
-
gem.add_dependency "logger", ">= 1.
|
|
26
|
+
gem.add_dependency "redis-client", ">= 0.26.0"
|
|
27
|
+
gem.add_dependency "connection_pool", ">= 3.0.0"
|
|
28
|
+
gem.add_dependency "rack", ">= 3.2.0"
|
|
29
|
+
gem.add_dependency "json", ">= 2.16.0"
|
|
30
|
+
gem.add_dependency "logger", ">= 1.7.0"
|
|
31
31
|
end
|
|
@@ -448,9 +448,7 @@ canvas + .table_container {
|
|
|
448
448
|
}
|
|
449
449
|
|
|
450
450
|
.buttons-row {
|
|
451
|
-
margin-top: var(--space-3x);
|
|
452
451
|
display: flex;
|
|
453
|
-
flex-wrap: wrap;
|
|
454
452
|
gap: 8px;
|
|
455
453
|
}
|
|
456
454
|
|
|
@@ -536,6 +534,8 @@ input[type="checkbox"] { accent-color: var(--color-primary); }
|
|
|
536
534
|
|
|
537
535
|
.pagination a { text-decoration: none; }
|
|
538
536
|
|
|
537
|
+
.pagination .active a { color: var(--color-primary); font-weight: 700; }
|
|
538
|
+
|
|
539
539
|
.pagination .disabled { opacity: 0.3; }
|
|
540
540
|
|
|
541
541
|
div:has(.pagination.pull-right) { margin-left: auto; }
|
data/web/locales/ar.yml
CHANGED
data/web/locales/fa.yml
CHANGED
data/web/locales/gd.yml
CHANGED