diff --git a/web/Gemfile.lock b/web/Gemfile.lock index 5b09a6d26..5c743f345 100644 --- a/web/Gemfile.lock +++ b/web/Gemfile.lock @@ -199,9 +199,9 @@ GEM language_server-protocol (3.17.0.6) lint_roller (1.1.0) logger (1.7.0) - loofah (2.25.2) + loofah (2.25.3) crass (~> 1.0.2) - nokogiri (>= 1.12.0) + nokogiri (>= 1.19.5) mail (2.9.1) logger mini_mime (>= 0.1.1) @@ -235,21 +235,21 @@ GEM net-smtp (0.5.2) net-protocol nio4r (2.7.5) - nokogiri (1.19.4-aarch64-linux-gnu) + nokogiri (1.19.7-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.4-aarch64-linux-musl) + nokogiri (1.19.7-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.4-arm-linux-gnu) + nokogiri (1.19.7-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.19.4-arm-linux-musl) + nokogiri (1.19.7-arm-linux-musl) racc (~> 1.4) - nokogiri (1.19.4-arm64-darwin) + nokogiri (1.19.7-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.4-x86_64-darwin) + nokogiri (1.19.7-x86_64-darwin) racc (~> 1.4) - nokogiri (1.19.4-x86_64-linux-gnu) + nokogiri (1.19.7-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.4-x86_64-linux-musl) + nokogiri (1.19.7-x86_64-linux-musl) racc (~> 1.4) nostr (0.7.0) bech32 (~> 1.4) @@ -319,8 +319,8 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.7.1) - loofah (~> 2.25, >= 2.25.2) + rails-html-sanitizer (1.7.2) + loofah (~> 2.25, >= 2.25.3) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) railties (8.1.4) actionpack (= 8.1.4) @@ -635,7 +635,7 @@ CHECKSUMS language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0 lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 + loofah (2.25.3) sha256=545f14eef0cc2a7de80df560530d593232b7c07e515c5c4e37532d9cb5b0185c mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8 marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b @@ -653,14 +653,14 @@ CHECKSUMS net-protocol (0.4.0) sha256=f9f90c8c6f64794a45160bf422177a336db12809de3beeeaa26b88fcd2e8c853 net-smtp (0.5.2) sha256=35103cc04d6b2d94632f9928ede07483c24b70b9c0db1f231fea43909b678f3c nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 - nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f - nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af - nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca - nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb - nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527 - nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551 - nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a - nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b + nokogiri (1.19.7-aarch64-linux-gnu) sha256=8f5e15d299602456406e1f03a9f3456cceb33a0722811e818d2ee2214eb4c84a + nokogiri (1.19.7-aarch64-linux-musl) sha256=efe0c3602e6e54521bc21c681930ce5568e2d11203de17400710b2b3ca2e4cb4 + nokogiri (1.19.7-arm-linux-gnu) sha256=7fd1907125691aa89e838cc91161761c758b4e17bb32f0dd9b5515a82cc759af + nokogiri (1.19.7-arm-linux-musl) sha256=28d0ff0c87b6bb2e56580323a6fb049d5d4a02b94f0958fdd47e9ac146991c77 + nokogiri (1.19.7-arm64-darwin) sha256=f27d7f25f4873fbc3aa35a2cc2fb680c98555fec1925348da439d0869d541142 + nokogiri (1.19.7-x86_64-darwin) sha256=508e81e8c5f84aaa240dce981bf1c43faae60808726f0ce7024ee92effc5cafe + nokogiri (1.19.7-x86_64-linux-gnu) sha256=25865ccb462f714e712fdf8906fde07849cb6704a808e3d629d75baa5af5dd3b + nokogiri (1.19.7-x86_64-linux-musl) sha256=464d1f3dfb0d53cf54c0c6a7e23e8e91cf68ec3cafc2b27dc4a7682f2d9bf546 nostr (0.7.0) sha256=2971efb01821857203d5e32569b3ab1bfa0f4904da4b9c1a7822112e904ccd90 oj (3.17.7) sha256=7a21d3ac8e544a850f1cdaf711b0c8144ba587d14d3e4917c0855fd7b0483db7 openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2 @@ -688,7 +688,7 @@ CHECKSUMS rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 rails (8.1.4) sha256=5f3ecdf9a2a3b79b2e31b8fee3b64a00f7284d76e5631f87d0e58e665911a33b rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d - rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + rails-html-sanitizer (1.7.2) sha256=a21ef45b7a6639fc8e2282473c547514cb86ae271616162a946bf89148fa2424 railties (8.1.4) sha256=b5e7a8e8984e33114d68af62227971960aae1e6c02bd7c9d826ffad97fff583c rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 diff --git a/web/app/jobs/pos_payment_reconciliation_job.rb b/web/app/jobs/pos_payment_reconciliation_job.rb index c821d542e..2cd4522dd 100644 --- a/web/app/jobs/pos_payment_reconciliation_job.rb +++ b/web/app/jobs/pos_payment_reconciliation_job.rb @@ -7,9 +7,8 @@ def perform # SQLite deployments share one filesystem. An advisory process lock keeps # a slow gateway request from overlapping a later scheduled sweep; it is # released automatically if the worker exits. - directory = Rails.root.join('tmp/locks') - FileUtils.mkdir_p(directory) - File.open(directory.join('pos-payment-reconciliation.lock'), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock| + FileUtils.mkdir_p(lock_path.dirname) + File.open(lock_path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock| return unless lock.flock(File::LOCK_EX | File::LOCK_NB) reconcile_batch @@ -20,6 +19,10 @@ def perform private + def lock_path + Rails.root.join('tmp/locks/pos-payment-reconciliation.lock') + end + def reconcile_batch PosPaymentAttempt.unscoped.joins(:tenant).where(tenants: { status: 'active' }) .where(status: %w[submitting unknown]).where('submitted_at < ?', 1.minute.ago) diff --git a/web/app/services/host_vitals.rb b/web/app/services/host_vitals.rb index f24e2aa1d..2dbd2ca47 100644 --- a/web/app/services/host_vitals.rb +++ b/web/app/services/host_vitals.rb @@ -68,14 +68,14 @@ def cgroup_cpu_usec # Without a cgroup (a laptop, say): the one-minute load average as a share of the cores. def load_percent - load1 = File.read('/proc/loadavg').split.first.to_f + load1 = read_host_file('/proc/loadavg').split.first.to_f [(load1 / Etc.nprocessors * 100).round, 100].min rescue SystemCallError, ZeroDivisionError nil end def memory_percent - info = File.read('/proc/meminfo').lines.to_h do |line| + info = read_host_file('/proc/meminfo').lines.to_h do |line| key, value = line.split(':') [key, value.to_i] end @@ -86,6 +86,11 @@ def memory_percent nil end + # Linux-only /proc files; tests stub this so the parsing is checked on any host. + def read_host_file(path) + File.read(path) + end + # Use of the filesystem holding the database, as df reports it. def disk_percent output = IO.popen(['df', '-Pk', database_path.dirname.to_s], err: File::NULL, &:read) diff --git a/web/app/services/instance_overview.rb b/web/app/services/instance_overview.rb index 2854566f4..5f7a97ecb 100644 --- a/web/app/services/instance_overview.rb +++ b/web/app/services/instance_overview.rb @@ -94,7 +94,11 @@ def instance end def deployment_label - value = ENV['RUPERT_INSTANCE_LABEL'].to_s.strip + # Read as UTF-8 whatever the server's locale; under a C/POSIX locale ENV values arrive as binary. + value = ENV['RUPERT_INSTANCE_LABEL'].to_s.dup.force_encoding(Encoding::UTF_8) + return 'Not configured' unless value.valid_encoding? + + value = value.strip value.present? && value.length <= 80 && !value.match?(/[\x00-\x1f\x7f]/) ? value : 'Not configured' end diff --git a/web/test/integration/platform_test.rb b/web/test/integration/platform_test.rb index 06d1b45c7..716b18815 100644 --- a/web/test/integration/platform_test.rb +++ b/web/test/integration/platform_test.rb @@ -206,6 +206,8 @@ class PlatformTest < ActionDispatch::IntegrationTest %w[PRIVATE_SYNC_DETAILS PRIVATE_BACKUP_FILENAME PRIVATE_BACKUP_LINK].each { |value| refute_includes response.body, value } ENV['RUPERT_INSTANCE_LABEL'] = "Invalid\nlabel" assert_equal 'Not configured', InstanceOverview.call(actor: @owner)[:instance][:label] + ENV['RUPERT_INSTANCE_LABEL'] = "Not UTF-8 \xFF".b + assert_equal 'Not configured', InstanceOverview.call(actor: @owner)[:instance][:label] ensure ENV['RUPERT_INSTANCE_LABEL'] = old_label end diff --git a/web/test/services/host_vitals_test.rb b/web/test/services/host_vitals_test.rb index 34d2db15a..6b96338d0 100644 --- a/web/test/services/host_vitals_test.rb +++ b/web/test/services/host_vitals_test.rb @@ -3,12 +3,41 @@ require 'test_helper' class HostVitalsTest < ActiveSupport::TestCase - test 'reads load, memory, disk and database size from this host' do + # /proc is Linux-only, so these readings are stubbed and the parsing is checked on any host (CI or a Mac). + PROC_FILES = { + '/proc/loadavg' => "1.50 0.80 0.40 2/345 6789\n", + '/proc/meminfo' => "MemTotal: 16000000 kB\nMemFree: 2000000 kB\nMemAvailable: 4000000 kB\n" + }.freeze + + test 'reads load, memory, disk and database size' do + HostVitals.stub(:cgroup_cpu_usec, nil) do + HostVitals.stub(:read_host_file, ->(path) { PROC_FILES.fetch(path) }) do + Etc.stub(:nprocessors, 4) do + reading = HostVitals.read + assert_equal 38, reading.cpu, 'one-minute load 1.5 on 4 cores' + assert_equal 75, reading.memory, '12 of 16 GB in use' + assert reading.disk.between?(0, 100) + assert reading.db_bytes.positive? + end + end + end + end + + test 'a reading this host cannot take is nil, never 0' do + HostVitals.stub(:cgroup_cpu_usec, nil) do + HostVitals.stub(:read_host_file, ->(path) { raise Errno::ENOENT, path }) do + reading = HostVitals.read + assert_nil reading.cpu + assert_nil reading.memory + end + end + end + + test 'reads CPU and memory from a real Linux host' do + skip 'needs Linux /proc' unless File.readable?('/proc/meminfo') reading = HostVitals.read assert_kind_of Integer, reading.cpu assert reading.memory.between?(0, 100) - assert reading.disk.between?(0, 100) - assert reading.db_bytes.positive? end test 'CPU is this container\'s own CPU time, not the host load average' do diff --git a/web/test/services/pos_card_payment_test.rb b/web/test/services/pos_card_payment_test.rb index 68296ec0f..9308b506b 100644 --- a/web/test/services/pos_card_payment_test.rb +++ b/web/test/services/pos_card_payment_test.rb @@ -3,6 +3,9 @@ require 'test_helper' class PosCardPaymentTest < ActiveSupport::TestCase setup do + # Parallel test workers would otherwise share the job's one lock file, and a job that finds it held skips its sweep. + @lock_dir = Dir.mktmpdir('pos-reconciliation-lock') + PosPaymentReconciliationJob.any_instance.stubs(:lock_path).returns(Pathname(@lock_dir).join('reconciliation.lock')) Current.tenant = tenants(:default_tenant) @location = Location.create!(source: 'ctrax', externalId: 'card-fixture/1', name: 'Card fixture') @user = users(:regular_user) @@ -40,7 +43,10 @@ class PosCardPaymentTest < ActiveSupport::TestCase { status: 200, headers: { 'Content-Type' => 'application/json' }, body: data.merge(messages: { resultCode: 'Ok' }).to_json } end end - teardown { Current.reset } + teardown do + Current.reset + FileUtils.rm_rf(@lock_dir) + end test 'reconciliation rotates past unresolved attempts without sending charges' do 51.times do |i|