Skip to content

Commit d6cc79e

Browse files
authored
Fix history saving when directory writability is unreliable (#1220)
* Fix history saving when directory writability is unreliable * Use Windows history regression test * Share history test setup
1 parent bf32202 commit d6cc79e

4 files changed

Lines changed: 129 additions & 47 deletions

File tree

lib/irb/history.rb

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,6 @@ def load_history
6464
def save_history
6565
return unless History.save_history?
6666
return unless (history_file = History.history_file)
67-
unless ensure_history_file_writable(history_file)
68-
warn <<~WARN
69-
Can't write history to #{History.history_file.inspect} due to insufficient permissions.
70-
Please verify the value of `IRB.conf[:HISTORY_FILE]`. Ensure the folder exists and that both the folder and file (if it exists) are writable.
71-
WARN
72-
return
73-
end
7467

7568
history = self.class::HISTORY.to_a
7669

@@ -80,36 +73,43 @@ def save_history
8073
append_history = true
8174
end
8275

83-
File.open(history_file, (append_history ? "a" : "w"), 0o600, encoding: IRB.conf[:LC_MESSAGES]&.encoding) do |f|
84-
hist = history.map { |l| l.scrub.split("\n").join("\\\n") }
76+
begin
77+
ensure_history_file_permissions(history_file)
8578

86-
unless append_history || History.infinite?
87-
# Check size before slicing because array.last(huge_number) raises RangeError.
88-
hist = hist.last(History.save_history) if hist.size > History.save_history
89-
end
79+
File.open(history_file, (append_history ? "a" : "w"), 0o600, encoding: IRB.conf[:LC_MESSAGES]&.encoding) do |f|
80+
hist = history.map { |l| l.scrub.split("\n").join("\\\n") }
81+
82+
unless append_history || History.infinite?
83+
# Check size before slicing because array.last(huge_number) raises RangeError.
84+
hist = hist.last(History.save_history) if hist.size > History.save_history
85+
end
9086

91-
f.puts(hist)
87+
f.puts(hist)
88+
end
89+
rescue Errno::ENOENT
90+
warn <<~WARN
91+
Can't write history to #{History.history_file.inspect} because the folder does not exist.
92+
Please verify the value of `IRB.conf[:HISTORY_FILE]`. Ensure the folder exists.
93+
WARN
94+
rescue Errno::EACCES, Errno::EPERM
95+
warn <<~WARN
96+
Can't write history to #{History.history_file.inspect} due to insufficient permissions.
97+
Please verify the value of `IRB.conf[:HISTORY_FILE]`. Ensure the folder exists and that both the folder and file (if it exists) are writable.
98+
WARN
9299
end
93100
end
94101

95102
private
96103

97-
# Returns boolean whether writing to +history_file+ will be possible.
98104
# Permissions of already existing +history_file+ are changed to
99105
# owner-only-readable if necessary [BUG #7694].
100-
def ensure_history_file_writable(history_file)
106+
def ensure_history_file_permissions(history_file)
101107
history_file = Pathname.new(history_file)
102108

103-
return false unless history_file.dirname.writable?
104-
return true unless history_file.exist?
109+
return unless history_file.exist?
105110

106-
begin
107-
if history_file.stat.mode & 0o66 != 0
108-
history_file.chmod 0o600
109-
end
110-
true
111-
rescue Errno::EPERM # no permissions
112-
false
111+
if history_file.stat.mode & 0o66 != 0
112+
history_file.chmod 0o600
113113
end
114114
end
115115
end

test/irb/history_test_case.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: false
2+
require "irb"
3+
require "fileutils"
4+
require "tmpdir"
5+
6+
require_relative "helper"
7+
8+
module TestIRB
9+
class HistoryTestCase < TestCase
10+
def setup
11+
@conf_backup = IRB.conf.dup
12+
@original_verbose, $VERBOSE = $VERBOSE, nil
13+
@tmpdir = Dir.mktmpdir("test_irb_history_")
14+
setup_envs(home: @tmpdir)
15+
IRB.conf[:LC_MESSAGES] = IRB::Locale.new
16+
save_encodings
17+
IRB.instance_variable_set(:@existing_rc_name_generators, nil)
18+
end
19+
20+
def teardown
21+
IRB.conf.replace(@conf_backup)
22+
IRB.instance_variable_set(:@existing_rc_name_generators, nil)
23+
teardown_envs
24+
restore_encodings
25+
$VERBOSE = @original_verbose
26+
FileUtils.rm_rf(@tmpdir)
27+
end
28+
end
29+
end

test/irb/test_history.rb

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# frozen_string_literal: false
2-
require 'irb'
32
require "tempfile"
43

5-
require_relative "helper"
4+
require_relative "history_test_case"
65

76
return if RUBY_PLATFORM.match?(/solaris|mswin|mingw/i)
87

@@ -14,26 +13,7 @@ module TestIRB
1413
Readline = ::Reline
1514
end
1615

17-
class HistoryTest < TestCase
18-
def setup
19-
@conf_backup = IRB.conf.dup
20-
@original_verbose, $VERBOSE = $VERBOSE, nil
21-
@tmpdir = Dir.mktmpdir("test_irb_history_")
22-
setup_envs(home: @tmpdir)
23-
IRB.conf[:LC_MESSAGES] = IRB::Locale.new
24-
save_encodings
25-
IRB.instance_variable_set(:@existing_rc_name_generators, nil)
26-
end
27-
28-
def teardown
29-
IRB.conf.replace(@conf_backup)
30-
IRB.instance_variable_set(:@existing_rc_name_generators, nil)
31-
teardown_envs
32-
restore_encodings
33-
$VERBOSE = @original_verbose
34-
FileUtils.rm_rf(@tmpdir)
35-
end
36-
16+
class HistoryTest < HistoryTestCase
3717
class TestInputMethodWithRelineHistory < TestInputMethod
3818
# When IRB.conf[:USE_MULTILINE] is true, IRB::RelineInputMethod uses Reline::History
3919
HISTORY = Reline::History.new(Reline.core.config)

test/irb/test_history_windows.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: false
2+
require_relative "history_test_case"
3+
4+
module TestIRB
5+
class HistoryWindowsTest < HistoryTestCase
6+
class TestInputMethodWithHistory < TestInputMethod
7+
HISTORY = []
8+
9+
include IRB::HistorySavingAbility
10+
11+
def gets
12+
super&.tap do |line|
13+
HISTORY << line unless line.empty?
14+
end
15+
end
16+
end
17+
18+
def test_history_is_saved_in_readonly_attributed_directory
19+
omit "Windows-only" unless windows?
20+
21+
history_dir = File.join(@tmpdir, "history")
22+
history_file = File.join(history_dir, "irb_history")
23+
FileUtils.mkdir_p(history_dir)
24+
25+
with_readonly_attribute(history_dir) do
26+
assert_nothing_raised do
27+
File.write(File.join(history_dir, "manual_write"), "ok")
28+
end
29+
30+
_output, warning = capture_output do
31+
run_irb_with_history(history_file)
32+
end
33+
34+
assert_equal("", warning)
35+
assert_equal(<<~HISTORY, File.read(history_file))
36+
puts 'history_entry'
37+
exit
38+
HISTORY
39+
end
40+
end
41+
42+
private
43+
44+
def with_readonly_attribute(path)
45+
assert(system("attrib", "+R", windows_path(path)), "failed to set read-only attribute on #{path}")
46+
yield
47+
ensure
48+
system("attrib", "-R", windows_path(path)) if path && File.directory?(path)
49+
end
50+
51+
def windows_path(path)
52+
return path unless File::ALT_SEPARATOR
53+
54+
path.tr(File::SEPARATOR, File::ALT_SEPARATOR)
55+
end
56+
57+
def run_irb_with_history(history_file)
58+
IRB.init_config(nil)
59+
IRB.init_error
60+
IRB.conf[:PROMPT_MODE] = :SIMPLE
61+
IRB.conf[:SAVE_HISTORY] = 100
62+
IRB.conf[:HISTORY_FILE] = history_file
63+
IRB.conf[:USE_PAGER] = false
64+
65+
input = TestInputMethodWithHistory.new(["puts 'history_entry'", "exit"])
66+
irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input)
67+
irb.context.return_format = "=> %s\n"
68+
irb.run(IRB.conf)
69+
ensure
70+
TestInputMethodWithHistory::HISTORY.clear
71+
end
72+
end
73+
end

0 commit comments

Comments
 (0)