-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
[rb] Lock the starting port in a file rather than on the port below it #17893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
d478a87
9619b06
354bcfc
2e42e50
130c925
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Licensed to the Software Freedom Conservancy (SFC) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The SFC licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| require 'tmpdir' | ||
|
|
||
| module Selenium | ||
| module WebDriver | ||
| # | ||
| # Holds a lock on a starting port so that two processes probing for a free port | ||
| # cannot both claim it. The lock lives in a file rather than on a TCP port, so it | ||
| # needs no port of its own and is released even if the process is killed. | ||
| # | ||
| # @api private | ||
| # | ||
|
|
||
| class PortLock | ||
| def initialize(port, timeout) | ||
| @path = File.join(Dir.tmpdir, "selenium-port-#{port}.lock") | ||
| @timeout = timeout | ||
| end | ||
|
|
||
| # | ||
| # Attempt to acquire the lock. Control is yielded to an execution block once it | ||
| # is held, and the lock is released when the block finishes. | ||
| # | ||
|
|
||
| def locked | ||
| file = lock | ||
|
|
||
| begin | ||
| yield | ||
| ensure | ||
| release(file) | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def lock | ||
| max_time = current_time + @timeout | ||
|
|
||
| loop do | ||
| file = open_lock_file | ||
| return file if file&.flock(File::LOCK_EX | File::LOCK_NB) | ||
|
|
||
| file&.close | ||
| break if current_time >= max_time | ||
|
|
||
| sleep 0.1 | ||
| end | ||
|
|
||
| raise Error::WebDriverError, "unable to acquire #{@path} within #{@timeout} seconds" | ||
| end | ||
|
|
||
| # nil means the lock is not available yet: Windows refuses to open a file another | ||
| # process has locked. The handle outlives this method when it is returned, since it | ||
| # holds the lock until #locked closes it. | ||
| def open_lock_file | ||
| file = File.open(@path, File::RDWR | File::CREAT, 0o600) # rubocop:disable Style/FileOpen | ||
| file.close_on_exec = true | ||
| file | ||
| rescue Errno::EROFS => e | ||
| raise Error::WebDriverError, "unable to create the lock file #{@path}: #{e.message}" | ||
| rescue Errno::EACCES => e | ||
|
Comment on lines
+78
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Misleading erofs error text PortLock#open_lock_file raises a WebDriverError saying it was unable to "create" the lock file on Errno::EROFS, but Errno::EROFS can also occur when opening an already-existing lock file for read/write on a read-only filesystem. This makes failures harder to diagnose, and the new spec locks in the misleading wording. Agent Prompt
|
||
| WebDriver.logger.debug("#{self}: #{e.message}", id: :driver_service) | ||
| nil | ||
| end | ||
|
|
||
| def release(file) | ||
| file.flock(File::LOCK_UN) | ||
| file.close | ||
| end | ||
|
|
||
| def current_time | ||
| Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
| end | ||
| end # PortLock | ||
| end # WebDriver | ||
| end # Selenium | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Licensed to the Software Freedom Conservancy (SFC) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The SFC licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| require File.expand_path('../spec_helper', __dir__) | ||
|
|
||
| module Selenium | ||
| module WebDriver | ||
| describe PortLock do | ||
| subject(:port_lock) { described_class.new(port, 2) } | ||
|
|
||
| # The lock file is named after the port, so a fixed one would collide with a real | ||
| # driver on 4444 and with any other run of this spec on the same machine. | ||
| let(:port) { 40_000 + (Process.pid % 10_000) } | ||
|
|
||
| it 'yields to the block' do | ||
| expect { |block| port_lock.locked(&block) }.to yield_control | ||
| end | ||
|
|
||
| it 'returns what the block returned' do | ||
| expect(port_lock.locked { :started }).to be(:started) | ||
| end | ||
|
|
||
| it 'releases the lock once the block is done' do | ||
| port_lock.locked { :first } | ||
|
|
||
| expect(described_class.new(port, 2).locked { :second }).to be(:second) | ||
| end | ||
|
|
||
| it 'releases the lock when the block raises' do | ||
| expect { port_lock.locked { raise 'boom' } }.to raise_error('boom') | ||
| expect(described_class.new(port, 2).locked { :second }).to be(:second) | ||
| end | ||
|
|
||
| it 'ignores a neighbouring port being in use' do | ||
| neighbour = TCPServer.new(Platform.localhost, 0) | ||
| busy = described_class.new(neighbour.addr[1], 2) | ||
|
|
||
| expect(busy.locked { :started }).to be(:started) | ||
| ensure | ||
| neighbour&.close | ||
| end | ||
|
|
||
| it 'keeps a second lock on the same port out' do | ||
| expect { | ||
| port_lock.locked { described_class.new(port, 0).locked { :never } } | ||
| }.to raise_error(Error::WebDriverError, /unable to acquire/) | ||
| end | ||
|
|
||
| it 'lets a lock on a different port through' do | ||
| expect(port_lock.locked { described_class.new(port + 1, 0).locked { :other } }).to be(:other) | ||
| end | ||
|
|
||
| it 'fails without waiting out the timeout when the lock file cannot be created' do | ||
| allow(File).to receive(:open).and_raise(Errno::EROFS) | ||
|
|
||
| expect { port_lock.locked { :never } } | ||
|
Comment on lines
+69
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Stubbed file.open in spec The new unit test stubs File.open using RSpec, which violates the requirement to avoid mocks unless backed by a contract-driven integration. This can reduce test fidelity by asserting behavior against a mock rather than the real filesystem behavior. Agent Prompt
|
||
| .to raise_error(Error::WebDriverError, /unable to create the lock file/) | ||
| end | ||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Tmp lockfile path hijack
🐞 Bug⛨ SecurityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools