sup 0.9.1 → 0.10
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sup might be problematic. Click here for more details.
- data/CONTRIBUTORS +10 -6
- data/History.txt +11 -0
- data/ReleaseNotes +10 -0
- data/bin/sup +55 -19
- data/bin/sup-add +18 -8
- data/bin/sup-config +2 -2
- data/bin/sup-convert-ferret-index +84 -0
- data/bin/sup-dump +4 -3
- data/bin/sup-sync +4 -3
- data/bin/sup-sync-back +3 -2
- data/bin/sup-tweak-labels +3 -3
- data/lib/sup.rb +35 -4
- data/lib/sup/buffer.rb +12 -6
- data/lib/sup/colormap.rb +1 -0
- data/lib/sup/crypto.rb +76 -55
- data/lib/sup/ferret_index.rb +6 -1
- data/lib/sup/index.rb +62 -8
- data/lib/sup/logger.rb +2 -1
- data/lib/sup/maildir.rb +4 -2
- data/lib/sup/mbox/loader.rb +4 -3
- data/lib/sup/message-chunks.rb +9 -7
- data/lib/sup/message.rb +29 -27
- data/lib/sup/mode.rb +11 -4
- data/lib/sup/modes/buffer-list-mode.rb +5 -0
- data/lib/sup/modes/console-mode.rb +4 -0
- data/lib/sup/modes/edit-message-mode.rb +4 -2
- data/lib/sup/modes/file-browser-mode.rb +1 -1
- data/lib/sup/modes/inbox-mode.rb +18 -1
- data/lib/sup/modes/label-list-mode.rb +44 -3
- data/lib/sup/modes/text-mode.rb +1 -1
- data/lib/sup/modes/thread-index-mode.rb +63 -52
- data/lib/sup/modes/thread-view-mode.rb +68 -7
- data/lib/sup/poll.rb +20 -5
- data/lib/sup/source.rb +1 -0
- data/lib/sup/thread.rb +1 -1
- data/lib/sup/util.rb +49 -11
- data/lib/sup/xapian_index.rb +151 -112
- metadata +4 -10
- data/lib/sup/hook.rb.BACKUP.8625.rb +0 -158
- data/lib/sup/hook.rb.BACKUP.8681.rb +0 -158
- data/lib/sup/hook.rb.BASE.8625.rb +0 -155
- data/lib/sup/hook.rb.BASE.8681.rb +0 -155
- data/lib/sup/hook.rb.LOCAL.8625.rb +0 -142
- data/lib/sup/hook.rb.LOCAL.8681.rb +0 -142
- data/lib/sup/hook.rb.REMOTE.8625.rb +0 -145
- data/lib/sup/hook.rb.REMOTE.8681.rb +0 -145
data/lib/sup.rb
CHANGED
@@ -37,7 +37,7 @@ class Module
|
|
37
37
|
end
|
38
38
|
|
39
39
|
module Redwood
|
40
|
-
VERSION = "0.
|
40
|
+
VERSION = "0.10"
|
41
41
|
|
42
42
|
BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
|
43
43
|
CONFIG_FN = File.join(BASE_DIR, "config.yaml")
|
@@ -54,7 +54,7 @@ module Redwood
|
|
54
54
|
YAML_DOMAIN = "masanjin.net"
|
55
55
|
YAML_DATE = "2006-10-01"
|
56
56
|
|
57
|
-
|
57
|
+
DEFAULT_NEW_INDEX_TYPE = 'xapian'
|
58
58
|
|
59
59
|
## record exceptions thrown in threads nicely
|
60
60
|
@exceptions = []
|
@@ -188,8 +188,38 @@ EOM
|
|
188
188
|
end
|
189
189
|
end
|
190
190
|
|
191
|
+
## to be called by entry points in bin/, to ensure that
|
192
|
+
## their versions match up against the library versions.
|
193
|
+
##
|
194
|
+
## this is a perennial source of bug reports from people
|
195
|
+
## who both use git and have a gem version installed.
|
196
|
+
def check_library_version_against v
|
197
|
+
unless Redwood::VERSION == v
|
198
|
+
$stderr.puts <<EOS
|
199
|
+
Error: version mismatch!
|
200
|
+
The sup executable is at version #{v.inspect}.
|
201
|
+
The sup libraries are at version #{Redwood::VERSION.inspect}.
|
202
|
+
|
203
|
+
Your development environment may be picking up code from a
|
204
|
+
rubygems installation of sup.
|
205
|
+
|
206
|
+
If you're running from git with a commandline like
|
207
|
+
|
208
|
+
ruby -Ilib #{$0}
|
209
|
+
|
210
|
+
try this instead:
|
211
|
+
|
212
|
+
RUBY_INVOCATION="ruby -Ilib" ruby -Ilib #{$0}
|
213
|
+
|
214
|
+
You can also try `gem uninstall sup` and removing all Sup rubygems.
|
215
|
+
|
216
|
+
EOS
|
217
|
+
abort
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
191
221
|
module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
|
192
|
-
:report_broken_sources
|
222
|
+
:report_broken_sources, :check_library_version_against
|
193
223
|
end
|
194
224
|
|
195
225
|
## set up default configuration file
|
@@ -229,7 +259,8 @@ else
|
|
229
259
|
:confirm_top_posting => true,
|
230
260
|
:discard_snippets_from_encrypted_messages => false,
|
231
261
|
:default_attachment_save_dir => "",
|
232
|
-
:sent_source => "sup://sent"
|
262
|
+
:sent_source => "sup://sent",
|
263
|
+
:poll_interval => 300
|
233
264
|
}
|
234
265
|
begin
|
235
266
|
FileUtils.mkdir_p Redwood::BASE_DIR
|
data/lib/sup/buffer.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'etc'
|
2
2
|
require 'thread'
|
3
|
-
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'ncursesw'
|
6
|
+
rescue LoadError
|
7
|
+
require 'ncurses'
|
8
|
+
end
|
4
9
|
|
5
10
|
if defined? Ncurses
|
6
11
|
module Ncurses
|
@@ -470,7 +475,7 @@ EOS
|
|
470
475
|
end
|
471
476
|
end
|
472
477
|
|
473
|
-
def ask_for_filename domain, question, default=nil
|
478
|
+
def ask_for_filename domain, question, default=nil, allow_directory=false
|
474
479
|
answer = ask domain, question, default do |s|
|
475
480
|
if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
|
476
481
|
full = $1
|
@@ -495,7 +500,7 @@ EOS
|
|
495
500
|
answer =
|
496
501
|
if answer.empty?
|
497
502
|
spawn_modal "file browser", FileBrowserMode.new
|
498
|
-
elsif File.directory?(answer)
|
503
|
+
elsif File.directory?(answer) && !allow_directory
|
499
504
|
spawn_modal "file browser", FileBrowserMode.new(answer)
|
500
505
|
else
|
501
506
|
File.expand_path answer
|
@@ -530,7 +535,7 @@ EOS
|
|
530
535
|
end
|
531
536
|
|
532
537
|
def ask_for_contacts domain, question, default_contacts=[]
|
533
|
-
default = default_contacts.map { |s| s.to_s }.join(" ")
|
538
|
+
default = default_contacts.is_a?(String) ? default_contacts : default_contacts.map { |s| s.to_s }.join(", ")
|
534
539
|
default += " " unless default.empty?
|
535
540
|
|
536
541
|
recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
|
@@ -604,7 +609,7 @@ EOS
|
|
604
609
|
def ask_getch question, accept=nil
|
605
610
|
raise "impossible!" if @asking
|
606
611
|
|
607
|
-
accept = accept.split(//).map { |x| x
|
612
|
+
accept = accept.split(//).map { |x| x.ord } if accept
|
608
613
|
|
609
614
|
status, title = get_status_and_title @focus_buf
|
610
615
|
Ncurses.sync do
|
@@ -640,7 +645,7 @@ EOS
|
|
640
645
|
## returns true (y), false (n), or nil (ctrl-g / cancel)
|
641
646
|
def ask_yes_or_no question
|
642
647
|
case(r = ask_getch question, "ynYN")
|
643
|
-
when ?y, ?Y
|
648
|
+
when ?y.ord, ?Y.ord
|
644
649
|
true
|
645
650
|
when nil
|
646
651
|
nil
|
@@ -755,6 +760,7 @@ EOS
|
|
755
760
|
end
|
756
761
|
|
757
762
|
private
|
763
|
+
|
758
764
|
def default_status_bar buf
|
759
765
|
" [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
|
760
766
|
end
|
data/lib/sup/colormap.rb
CHANGED
@@ -50,6 +50,7 @@ class Colormap
|
|
50
50
|
:system_buf => { :fg => "blue", :bg => "default" },
|
51
51
|
:regular_buf => { :fg => "white", :bg => "default" },
|
52
52
|
:modified_buffer => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
|
53
|
+
:date => { :fg => "white", :bg => "default"},
|
53
54
|
}
|
54
55
|
|
55
56
|
def initialize
|
data/lib/sup/crypto.rb
CHANGED
@@ -15,15 +15,14 @@ class CryptoManager
|
|
15
15
|
@mutex = Mutex.new
|
16
16
|
|
17
17
|
bin = `which gpg`.chomp
|
18
|
-
@cmd =
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
18
|
+
@cmd = case bin
|
19
|
+
when /\S/
|
20
|
+
debug "crypto: detected gpg binary in #{bin}"
|
21
|
+
"#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
|
22
|
+
else
|
23
|
+
debug "crypto: no gpg binary detected"
|
24
|
+
nil
|
25
|
+
end
|
27
26
|
end
|
28
27
|
|
29
28
|
def have_crypto?; !@cmd.nil? end
|
@@ -33,15 +32,19 @@ class CryptoManager
|
|
33
32
|
payload_fn.write format_payload(payload)
|
34
33
|
payload_fn.close
|
35
34
|
|
36
|
-
|
35
|
+
sig_fn = Tempfile.new "redwood.signature"; sig_fn.close
|
37
36
|
|
38
|
-
|
37
|
+
message = run_gpg "--output #{sig_fn.path} --yes --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}", :interactive => true
|
38
|
+
unless $?.success?
|
39
|
+
info "Error while running gpg: #{message}"
|
40
|
+
raise Error, "GPG command failed. See log for details."
|
41
|
+
end
|
39
42
|
|
40
43
|
envelope = RMail::Message.new
|
41
44
|
envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha1'
|
42
45
|
|
43
46
|
envelope.add_part payload
|
44
|
-
signature = RMail::Message.make_attachment
|
47
|
+
signature = RMail::Message.make_attachment IO.read(sig_fn.path), "application/pgp-signature", nil, "signature.asc"
|
45
48
|
envelope.add_part signature
|
46
49
|
envelope
|
47
50
|
end
|
@@ -51,15 +54,20 @@ class CryptoManager
|
|
51
54
|
payload_fn.write format_payload(payload)
|
52
55
|
payload_fn.close
|
53
56
|
|
57
|
+
encrypted_fn = Tempfile.new "redwood.encrypted"; encrypted_fn.close
|
58
|
+
|
54
59
|
recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ")
|
55
60
|
sign_opts = sign ? "--sign --local-user '#{from}'" : ""
|
56
|
-
|
57
|
-
|
61
|
+
message = run_gpg "--output #{encrypted_fn.path} --yes --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}", :interactive => true
|
62
|
+
unless $?.success?
|
63
|
+
info "Error while running gpg: #{message}"
|
64
|
+
raise Error, "GPG command failed. See log for details."
|
65
|
+
end
|
58
66
|
|
59
67
|
encrypted_payload = RMail::Message.new
|
60
68
|
encrypted_payload.header["Content-Type"] = "application/octet-stream"
|
61
69
|
encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"'
|
62
|
-
encrypted_payload.body =
|
70
|
+
encrypted_payload.body = IO.read(encrypted_fn.path)
|
63
71
|
|
64
72
|
control = RMail::Message.new
|
65
73
|
control.header["Content-Type"] = "application/pgp-encrypted"
|
@@ -111,46 +119,50 @@ class CryptoManager
|
|
111
119
|
payload_fn.write payload.to_s
|
112
120
|
payload_fn.close
|
113
121
|
|
114
|
-
|
122
|
+
output_fn = Tempfile.new "redwood.output"
|
123
|
+
output_fn.close
|
115
124
|
|
116
|
-
|
117
|
-
decrypted_payload, sig_lines = if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
|
118
|
-
[$1, $2]
|
119
|
-
else
|
120
|
-
[output, nil]
|
121
|
-
end
|
125
|
+
message = run_gpg "--output #{output_fn.path} --yes --decrypt #{payload_fn.path}", :interactive => true
|
122
126
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
|
128
|
-
end
|
129
|
-
end
|
127
|
+
unless $?.success?
|
128
|
+
info "Error while running gpg: #{message}"
|
129
|
+
return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", message.split("\n"))
|
130
|
+
end
|
130
131
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
# hence being shown as an attachment. If we detect this is happening,
|
143
|
-
# we force the decrypted payload to be interpreted as MIME.
|
144
|
-
msg = RMail::Parser.read(decrypted_payload)
|
145
|
-
if msg.header.content_type =~ %r{^multipart/} and not msg.multipart?
|
146
|
-
decrypted_payload = "MIME-Version: 1.0\n" + decrypted_payload
|
147
|
-
msg = RMail::Parser.read(decrypted_payload)
|
148
|
-
end
|
149
|
-
notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
|
150
|
-
[notice, sig, msg]
|
151
|
-
else
|
152
|
-
Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
|
132
|
+
output = IO.read output_fn.path
|
133
|
+
output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding
|
134
|
+
|
135
|
+
## there's probably a better way to do this, but we're using the output to
|
136
|
+
## look for a valid signature being present.
|
137
|
+
|
138
|
+
sig = case message
|
139
|
+
when /^gpg: (Good signature from .*$)/i
|
140
|
+
Chunk::CryptoNotice.new :valid, $1, message.split("\n")
|
141
|
+
when /^gpg: (Bad signature from .*$)/i
|
142
|
+
Chunk::CryptoNotice.new :invalid, $1, message.split("\n")
|
153
143
|
end
|
144
|
+
|
145
|
+
# This is gross. This decrypted payload could very well be a multipart
|
146
|
+
# element itself, as opposed to a simple payload. For example, a
|
147
|
+
# multipart/signed element, like those generated by Mutt when encrypting
|
148
|
+
# and signing a message (instead of just clearsigning the body).
|
149
|
+
# Supposedly, decrypted_payload being a multipart element ought to work
|
150
|
+
# out nicely because Message::multipart_encrypted_to_chunks() runs the
|
151
|
+
# decrypted message through message_to_chunks() again to get any
|
152
|
+
# children. However, it does not work as intended because these inner
|
153
|
+
# payloads need not carry a MIME-Version header, yet they are fed to
|
154
|
+
# RMail as a top-level message, for which the MIME-Version header is
|
155
|
+
# required. This causes for the part not to be detected as multipart,
|
156
|
+
# hence being shown as an attachment. If we detect this is happening,
|
157
|
+
# we force the decrypted payload to be interpreted as MIME.
|
158
|
+
msg = RMail::Parser.read output
|
159
|
+
if msg.header.content_type =~ %r{^multipart/} && !msg.multipart?
|
160
|
+
output = "MIME-Version: 1.0\n" + output
|
161
|
+
output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding
|
162
|
+
msg = RMail::Parser.read output
|
163
|
+
end
|
164
|
+
notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
|
165
|
+
[notice, sig, msg]
|
154
166
|
end
|
155
167
|
|
156
168
|
private
|
@@ -169,10 +181,19 @@ private
|
|
169
181
|
payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
|
170
182
|
end
|
171
183
|
|
172
|
-
def run_gpg args
|
173
|
-
cmd = "#{@cmd} #{args}
|
174
|
-
|
175
|
-
|
184
|
+
def run_gpg args, opts={}
|
185
|
+
cmd = "#{@cmd} #{args}"
|
186
|
+
if opts[:interactive] && BufferManager.instantiated?
|
187
|
+
output_fn = Tempfile.new "redwood.output"
|
188
|
+
output_fn.close
|
189
|
+
cmd += " > #{output_fn.path} 2> /dev/null"
|
190
|
+
debug "crypto: running: #{cmd}"
|
191
|
+
BufferManager.shell_out cmd
|
192
|
+
IO.read(output_fn.path) rescue "can't read output"
|
193
|
+
else
|
194
|
+
debug "crypto: running: #{cmd}"
|
195
|
+
`#{cmd} 2> /dev/null`
|
196
|
+
end
|
176
197
|
end
|
177
198
|
end
|
178
199
|
end
|
data/lib/sup/ferret_index.rb
CHANGED
@@ -11,6 +11,8 @@ Variables:
|
|
11
11
|
subs: The string being searched.
|
12
12
|
EOS
|
13
13
|
|
14
|
+
def is_a_deprecated_ferret_index?; true end
|
15
|
+
|
14
16
|
def initialize dir=BASE_DIR
|
15
17
|
super
|
16
18
|
|
@@ -127,8 +129,11 @@ EOS
|
|
127
129
|
:from => (m.from ? m.from.indexable_content : ""),
|
128
130
|
:to => (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" "),
|
129
131
|
|
132
|
+
## always overwrite :refs.
|
133
|
+
## these might have changed due to manual thread joining.
|
134
|
+
:refs => (m.refs + m.replytos).uniq.join(" "),
|
135
|
+
|
130
136
|
:subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
|
131
|
-
:refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
|
132
137
|
}
|
133
138
|
|
134
139
|
@index_mutex.synchronize do
|
data/lib/sup/index.rb
CHANGED
@@ -6,7 +6,7 @@ begin
|
|
6
6
|
require 'chronic'
|
7
7
|
$have_chronic = true
|
8
8
|
rescue LoadError => e
|
9
|
-
debug "
|
9
|
+
debug "No 'chronic' gem detected. Install it for date/time query restrictions."
|
10
10
|
$have_chronic = false
|
11
11
|
end
|
12
12
|
|
@@ -23,11 +23,15 @@ class BaseIndex
|
|
23
23
|
def method_missing m; @h[m.to_s] end
|
24
24
|
end
|
25
25
|
|
26
|
+
def is_a_deprecated_ferret_index?; false end
|
27
|
+
|
26
28
|
include Singleton
|
27
29
|
|
28
30
|
def initialize dir=BASE_DIR
|
29
31
|
@dir = dir
|
30
32
|
@lock = Lockfile.new lockfile, :retries => 0, :max_age => nil
|
33
|
+
@sync_worker = nil
|
34
|
+
@sync_queue = Queue.new
|
31
35
|
end
|
32
36
|
|
33
37
|
def lockfile; File.join @dir, "lock" end
|
@@ -172,15 +176,65 @@ class BaseIndex
|
|
172
176
|
def parse_query s
|
173
177
|
unimplemented
|
174
178
|
end
|
179
|
+
|
180
|
+
def save_thread t
|
181
|
+
t.each_dirty_message do |m|
|
182
|
+
if @sync_worker
|
183
|
+
@sync_queue << m
|
184
|
+
else
|
185
|
+
update_message_state m
|
186
|
+
end
|
187
|
+
m.clear_dirty
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def start_sync_worker
|
192
|
+
@sync_worker = Redwood::reporting_thread('index sync') { run_sync_worker }
|
193
|
+
end
|
194
|
+
|
195
|
+
def stop_sync_worker
|
196
|
+
return unless worker = @sync_worker
|
197
|
+
@sync_worker = nil
|
198
|
+
@sync_queue << :die
|
199
|
+
worker.join
|
200
|
+
end
|
201
|
+
|
202
|
+
def run_sync_worker
|
203
|
+
while m = @sync_queue.deq
|
204
|
+
return if m == :die
|
205
|
+
update_message_state m
|
206
|
+
# Necessary to keep Xapian calls from lagging the UI too much.
|
207
|
+
sleep 0.03
|
208
|
+
end
|
209
|
+
end
|
175
210
|
end
|
176
211
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
212
|
+
## just to make the backtraces even more insane, here we engage in yet more
|
213
|
+
## method_missing metaprogramming so that Index.init(index_type_name) will
|
214
|
+
## magically make Index act like the correct Index class.
|
215
|
+
class Index
|
216
|
+
def self.init type=nil
|
217
|
+
## determine the index type from the many possible ways of setting it
|
218
|
+
type = (type == "auto" ? nil : type) ||
|
219
|
+
ENV['SUP_INDEX'] ||
|
220
|
+
$config[:index] ||
|
221
|
+
(File.exist?(File.join(BASE_DIR, "xapian")) && "xapian") || ## PRIORITIZE THIS
|
222
|
+
(File.exist?(File.join(BASE_DIR, "ferret")) && "ferret") || ## deprioritize this
|
223
|
+
DEFAULT_NEW_INDEX_TYPE
|
224
|
+
begin
|
225
|
+
require "sup/#{type}_index"
|
226
|
+
@klass = Redwood.const_get "#{type.capitalize}Index"
|
227
|
+
@obj = @klass.init
|
228
|
+
rescue LoadError, NameError => e
|
229
|
+
raise "unknown index type #{type.inspect}: #{e.message}"
|
230
|
+
end
|
231
|
+
debug "using #{type} index"
|
232
|
+
@obj
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.instance; @obj end
|
236
|
+
def self.method_missing m, *a, &b; @obj.send(m, *a, &b) end
|
237
|
+
def self.const_missing x; @obj.class.const_get(x) end
|
182
238
|
end
|
183
|
-
Index = Redwood.const_get "#{index_name.capitalize}Index"
|
184
|
-
debug "using index #{Index.name}"
|
185
239
|
|
186
240
|
end
|
data/lib/sup/logger.rb
CHANGED
@@ -14,13 +14,14 @@ class Logger
|
|
14
14
|
|
15
15
|
def initialize level=nil
|
16
16
|
level ||= ENV["SUP_LOG_LEVEL"] || "info"
|
17
|
-
|
17
|
+
self.level = level
|
18
18
|
@mutex = Mutex.new
|
19
19
|
@buf = StringIO.new
|
20
20
|
@sinks = []
|
21
21
|
end
|
22
22
|
|
23
23
|
def level; LEVELS[@level] end
|
24
|
+
def level=(level); @level = LEVELS.index(level) || raise(ArgumentError, "invalid log level #{level.inspect}: should be one of #{LEVELS * ', '}"); end
|
24
25
|
|
25
26
|
def add_sink s, copy_current=true
|
26
27
|
@mutex.synchronize do
|