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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +58 -0
  3. data/README.md +16 -1
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +11 -8
  7. data/lib/generators/sidekiq/job_generator.rb +15 -3
  8. data/lib/sidekiq/api.rb +130 -76
  9. data/lib/sidekiq/capsule.rb +0 -1
  10. data/lib/sidekiq/cli.rb +2 -1
  11. data/lib/sidekiq/client.rb +3 -1
  12. data/lib/sidekiq/component.rb +3 -0
  13. data/lib/sidekiq/config.rb +4 -5
  14. data/lib/sidekiq/job.rb +2 -0
  15. data/lib/sidekiq/job_retry.rb +7 -3
  16. data/lib/sidekiq/launcher.rb +5 -5
  17. data/lib/sidekiq/manager.rb +1 -1
  18. data/lib/sidekiq/paginator.rb +6 -1
  19. data/lib/sidekiq/profiler.rb +1 -1
  20. data/lib/sidekiq/scheduled.rb +6 -7
  21. data/lib/sidekiq/test_api.rb +331 -0
  22. data/lib/sidekiq/testing/inline.rb +2 -30
  23. data/lib/sidekiq/testing.rb +2 -334
  24. data/lib/sidekiq/tui/controls.rb +53 -0
  25. data/lib/sidekiq/tui/filtering.rb +53 -0
  26. data/lib/sidekiq/tui/tabs/base_tab.rb +187 -0
  27. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  28. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  29. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  30. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  31. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  32. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  33. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  34. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  35. data/lib/sidekiq/tui/tabs.rb +15 -0
  36. data/lib/sidekiq/tui.rb +380 -0
  37. data/lib/sidekiq/version.rb +1 -1
  38. data/lib/sidekiq/web/action.rb +1 -1
  39. data/lib/sidekiq/web/application.rb +2 -2
  40. data/lib/sidekiq/web/config.rb +3 -6
  41. data/lib/sidekiq/web/helpers.rb +43 -3
  42. data/lib/sidekiq/web.rb +23 -4
  43. data/lib/sidekiq.rb +7 -0
  44. data/sidekiq.gemspec +6 -6
  45. data/web/assets/javascripts/application.js +1 -1
  46. data/web/assets/stylesheets/style.css +2 -2
  47. data/web/locales/ar.yml +1 -1
  48. data/web/locales/fa.yml +1 -1
  49. data/web/locales/gd.yml +1 -1
  50. data/web/locales/he.yml +1 -1
  51. data/web/locales/pt-BR.yml +1 -1
  52. data/web/locales/ur.yml +1 -1
  53. data/web/locales/zh-TW.yml +1 -1
  54. data/web/views/{_paging.erb → _paging.html.erb} +1 -1
  55. data/web/views/{busy.erb → busy.html.erb} +1 -1
  56. data/web/views/{metrics.erb → metrics.html.erb} +3 -2
  57. metadata +51 -35
  58. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  59. /data/web/views/{_footer.erb → _footer.html.erb} +0 -0
  60. /data/web/views/{_job_info.erb → _job_info.html.erb} +0 -0
  61. /data/web/views/{_metrics_period_select.erb → _metrics_period_select.html.erb} +0 -0
  62. /data/web/views/{_nav.erb → _nav.html.erb} +0 -0
  63. /data/web/views/{_poll_link.erb → _poll_link.html.erb} +0 -0
  64. /data/web/views/{_summary.erb → _summary.html.erb} +0 -0
  65. /data/web/views/{dashboard.erb → dashboard.html.erb} +0 -0
  66. /data/web/views/{dead.erb → dead.html.erb} +0 -0
  67. /data/web/views/{filtering.erb → filtering.html.erb} +0 -0
  68. /data/web/views/{layout.erb → layout.html.erb} +0 -0
  69. /data/web/views/{metrics_for_job.erb → metrics_for_job.html.erb} +0 -0
  70. /data/web/views/{morgue.erb → morgue.html.erb} +0 -0
  71. /data/web/views/{profiles.erb → profiles.html.erb} +0 -0
  72. /data/web/views/{queue.erb → queue.html.erb} +0 -0
  73. /data/web/views/{queues.erb → queues.html.erb} +0 -0
  74. /data/web/views/{retries.erb → retries.html.erb} +0 -0
  75. /data/web/views/{retry.erb → retry.html.erb} +0 -0
  76. /data/web/views/{scheduled.erb → scheduled.html.erb} +0 -0
  77. /data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +0 -0
@@ -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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "8.0.10"
4
+ VERSION = "8.1.3"
5
5
  MAJOR = 8
6
6
 
7
7
  def self.gem_version
@@ -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.quiet! if url_params("quiet")
103
- pro.stop! if url_params("stop")
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?
@@ -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 = []
@@ -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 = YAML.safe_load_file(file)
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
- "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
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: ASSETS,
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.23.2"
27
- gem.add_dependency "connection_pool", ">= 2.5.0"
28
- gem.add_dependency "rack", ">= 3.1.0"
29
- gem.add_dependency "json", ">= 2.9.0"
30
- gem.add_dependency "logger", ">= 1.6.2"
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
@@ -154,7 +154,7 @@ function livePollCallback() {
154
154
 
155
155
  function checkResponse(resp) {
156
156
  if (!resp.ok) {
157
- throw response.error();
157
+ throw resp.error();
158
158
  }
159
159
  return resp
160
160
  }
@@ -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
@@ -74,7 +74,7 @@ ar:
74
74
  Stop: إيقاف
75
75
  StopAll: إيقاف الكل
76
76
  StopPolling: إيقاف الاستعلامات
77
- TextDirection: 'rtl'
77
+ TextDirection: rtl
78
78
  Thread: نيسب
79
79
  Threads: نياسب
80
80
  ThreeMonths: ثلاثة أشهر
data/web/locales/fa.yml CHANGED
@@ -69,7 +69,7 @@ fa:
69
69
  Stop: توقف
70
70
  StopAll: توقف همه
71
71
  StopPolling: Stop Polling
72
- TextDirection: 'rtl'
72
+ TextDirection: rtl
73
73
  Thread: رشته
74
74
  Threads: رشته ها
75
75
  ThreeMonths: ۳ ماه
data/web/locales/gd.yml CHANGED
@@ -1,4 +1,4 @@
1
- # elements like %{queue} are variables and should not be translated
1
+ # elements like %{queue} are variables and should not be translated
2
2
  gd:
3
3
  LanguageName: Gaeilge
4
4
  Actions: Gnìomhan
data/web/locales/he.yml CHANGED
@@ -69,7 +69,7 @@ he:
69
69
  Stop: עצור
70
70
  StopAll: עצור הכל
71
71
  StopPolling: עצור תשאול
72
- TextDirection: 'rtl'
72
+ TextDirection: rtl
73
73
  Thread: חוט
74
74
  Threads: חוטים
75
75
  ThreeMonths: 3 חדשים