sanguinews 0.60
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.
- checksums.yaml +7 -0
- data/bin/sanguinews +4 -0
- data/ext/yencoded/extconf.rb +3 -0
- data/ext/yencoded/yencoded.c +89 -0
- data/ext/yencoded/yencoded.h +6 -0
- data/lib/sanguinews.rb +415 -0
- data/lib/sanguinews/file_to_upload.rb +109 -0
- data/lib/sanguinews/nntp.rb +896 -0
- data/lib/sanguinews/nntp_msg.rb +82 -0
- data/lib/sanguinews/thread-pool.rb +150 -0
- data/lib/sanguinews/version.rb +3 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8fa336b8808a65611854b554245a288b4b05f303
|
4
|
+
data.tar.gz: 0c4309243350764bae4669b537f65d4ed9b79e12
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b231b17b59ab469a3d64294c6f3befc97abf0083ebfac5fce887e74511844591708f879de799b09dc75348f202d6bfd5aa1bb05bde52b8970960214922f6abba
|
7
|
+
data.tar.gz: e551da80f3d6f1641c4e47b46636b34ffcfd196fbd9f111c6fab804823df3112418669f38ecd5e3c2c34e4f8a25407f7616d0383d4146cfa4f281f4ab07a8c7f
|
data/bin/sanguinews
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////
|
2
|
+
// Yencoded - C code to yencode bindary data for sanguinews
|
3
|
+
// Copyright (c) 2013-2014, Tadeus Dobrovolskij
|
4
|
+
// This library is free software; you can redistribute it and/or modify
|
5
|
+
// it under the terms of the GNU General Public License as published by
|
6
|
+
// the Free Software Foundation; either version 2 of the License, or
|
7
|
+
// (at your option) any later version.
|
8
|
+
//
|
9
|
+
// This library is distributed in the hope that it will be useful,
|
10
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
// GNU General Public License for more details.
|
13
|
+
//
|
14
|
+
// You should have received a copy of the GNU General Public License along
|
15
|
+
// with this library; if not, write to the Free Software Foundation, Inc.,
|
16
|
+
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
17
|
+
/////////////////////////////////////////////////////////////////////////
|
18
|
+
#include <ruby.h>
|
19
|
+
#include <yencoded.h>
|
20
|
+
|
21
|
+
void Init_yencoded() {
|
22
|
+
Yencoded = rb_define_module("Yencoded");
|
23
|
+
YencodedData = rb_define_module_under(Yencoded, "Data");
|
24
|
+
rb_define_singleton_method(YencodedData, "yenc", method_yencoded_data_yenc, 2);
|
25
|
+
}
|
26
|
+
|
27
|
+
VALUE method_yencoded_data_yenc(VALUE self, VALUE data,VALUE length) {
|
28
|
+
char *bindata;
|
29
|
+
long datalen;
|
30
|
+
int i=0;
|
31
|
+
int linelen=128;
|
32
|
+
long restlen;
|
33
|
+
long destlen;
|
34
|
+
unsigned char c;
|
35
|
+
unsigned char *output;
|
36
|
+
unsigned char *start;
|
37
|
+
VALUE result;
|
38
|
+
|
39
|
+
// convert ruby variables to c variables
|
40
|
+
bindata = StringValuePtr(data);
|
41
|
+
datalen = FIX2LONG(length);
|
42
|
+
|
43
|
+
restlen = datalen; //restlen is our byte processing counter
|
44
|
+
destlen = restlen; //will be needing this for memory allocation
|
45
|
+
output = (unsigned char*)malloc(2 * destlen * sizeof(char));
|
46
|
+
start = output; //starting address will be stored here
|
47
|
+
while (restlen>0) {
|
48
|
+
c=(unsigned char) *bindata; //get byte
|
49
|
+
c=c+42; //add 42 as per yenc specs
|
50
|
+
bindata++; restlen--;
|
51
|
+
switch(c) { //special characters
|
52
|
+
case 0:
|
53
|
+
case 10:
|
54
|
+
case 13:
|
55
|
+
case 61:
|
56
|
+
destlen++; i++; //we need more memory than expected
|
57
|
+
*output=61; output++; //add escape char to output
|
58
|
+
c = c+64;
|
59
|
+
break;
|
60
|
+
case 9: //escape tab and space if the are first or last on the line
|
61
|
+
case 32:
|
62
|
+
if ((i==0)||(i==linelen-1)) {
|
63
|
+
destlen++; i++; //we need more memory than expected
|
64
|
+
*output=61; output++; //add escape char to output
|
65
|
+
c = c+64;
|
66
|
+
}
|
67
|
+
break;
|
68
|
+
case 46: //escape dot if it's in a first column
|
69
|
+
if (i==0) {
|
70
|
+
destlen++; i++; //we need more memory than expected
|
71
|
+
*output=61; output++; //add escape char to output
|
72
|
+
c = c+64;
|
73
|
+
}
|
74
|
+
break;
|
75
|
+
}
|
76
|
+
*output=c; output++; i++;
|
77
|
+
if ((i>=linelen)||(restlen==0)){
|
78
|
+
destlen++; destlen++;
|
79
|
+
*output=13; output++; //according to yenc specs we must use windows style line breaks
|
80
|
+
*output=10; output++;
|
81
|
+
i=0;
|
82
|
+
}
|
83
|
+
}
|
84
|
+
*output=0; //NULL termination is required
|
85
|
+
destlen++;
|
86
|
+
result=rb_str_new2((char*)start);
|
87
|
+
free(start);
|
88
|
+
return result;
|
89
|
+
}
|
data/lib/sanguinews.rb
ADDED
@@ -0,0 +1,415 @@
|
|
1
|
+
########################################################################
|
2
|
+
# sanguinews - usenet command line binary poster written in ruby
|
3
|
+
# Copyright (c) 2013, Tadeus Dobrovolskij
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License along
|
15
|
+
# with this program; if not, write to the Free Software Foundation, Inc.,
|
16
|
+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
17
|
+
########################################################################
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'bundler/setup'
|
21
|
+
require 'optparse'
|
22
|
+
require 'monitor'
|
23
|
+
require 'date'
|
24
|
+
require 'tempfile'
|
25
|
+
# Following non-standard gems are needed
|
26
|
+
require 'parseconfig'
|
27
|
+
require 'speedometer'
|
28
|
+
# Our library
|
29
|
+
require_relative 'sanguinews/thread-pool'
|
30
|
+
require_relative 'sanguinews/nntp'
|
31
|
+
require_relative 'sanguinews/nntp_msg'
|
32
|
+
require_relative 'sanguinews/file_to_upload'
|
33
|
+
require_relative 'sanguinews/yencoded'
|
34
|
+
require_relative 'sanguinews/version'
|
35
|
+
|
36
|
+
module Sanguinews
|
37
|
+
module_function
|
38
|
+
# Method returns yenc encoded string and crc32 value
|
39
|
+
def yencode(file, length, queue)
|
40
|
+
i = 1
|
41
|
+
until file.eof?
|
42
|
+
bindata = file.read(length)
|
43
|
+
# We can't take all memory, so we wait
|
44
|
+
queue.synchronize do
|
45
|
+
@cond.wait_while do
|
46
|
+
queue.length > @threads * 3
|
47
|
+
end
|
48
|
+
end
|
49
|
+
data = {}
|
50
|
+
final_data = []
|
51
|
+
len = bindata.length
|
52
|
+
data[:yenc] = Yencoded::Data.yenc(bindata, len)
|
53
|
+
data[:crc32] = Zlib.crc32(bindata, 0).to_s(16)
|
54
|
+
data[:length] = len
|
55
|
+
data[:chunk] = i
|
56
|
+
data[:file] = file
|
57
|
+
final_data[0] = form_message(data)
|
58
|
+
final_data[1] = file
|
59
|
+
queue.push(final_data)
|
60
|
+
i += 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def form_message(data)
|
65
|
+
message = data[:yenc]
|
66
|
+
length = data[:length]
|
67
|
+
pcrc32 = data[:crc32]
|
68
|
+
file = data[:file]
|
69
|
+
chunk = data[:chunk]
|
70
|
+
crc32 = file.crc32
|
71
|
+
fsize = file.size
|
72
|
+
chunks = file.chunks
|
73
|
+
basename = file.name
|
74
|
+
# usenet works with ASCII
|
75
|
+
subject="#{@prefix}#{file.dir_prefix}\"#{basename}\" yEnc (#{chunk}/#{chunks})"
|
76
|
+
msg = NntpMsg.new(@from, @groups, subject)
|
77
|
+
msg.poster = "sanguinews v#{Sanguinews::VERSION} (ruby #{RUBY_VERSION}) - https://github.com/tdobrovolskij/sanguinews"
|
78
|
+
msg.xna = @xna
|
79
|
+
msg.message = message.force_encoding('ASCII-8BIT')
|
80
|
+
msg.yenc_body(chunk, chunks, crc32, pcrc32, length, fsize, basename)
|
81
|
+
msg = msg.return_self
|
82
|
+
{ message: msg, filename: basename, chunk: chunk, length: length }
|
83
|
+
end
|
84
|
+
|
85
|
+
def connect(x)
|
86
|
+
begin
|
87
|
+
nntp = Net::NNTP.start(@server, @port, @username, @password, @mode)
|
88
|
+
rescue
|
89
|
+
@s.log([$!, $@], stderr: true) if @debug
|
90
|
+
if @verbose
|
91
|
+
parse_error($!.to_s)
|
92
|
+
@s.log("Connection nr. #{x} has failed. Reconnecting...\n", stderr: true)
|
93
|
+
end
|
94
|
+
sleep @delay
|
95
|
+
retry
|
96
|
+
end
|
97
|
+
return nntp
|
98
|
+
end
|
99
|
+
|
100
|
+
def parse_config(config)
|
101
|
+
config = ParseConfig.new(config)
|
102
|
+
config.get_params()
|
103
|
+
@username = config['username']
|
104
|
+
@password = config['password']
|
105
|
+
@from = config['from']
|
106
|
+
@server = config['server']
|
107
|
+
@port = config['port']
|
108
|
+
@threads = config['connections'].to_i
|
109
|
+
@length = config['article_size'].to_i
|
110
|
+
@delay = config['reconnect_delay'].to_i
|
111
|
+
@groups = config['groups']
|
112
|
+
@prefix = config['prefix']
|
113
|
+
ssl = config['ssl']
|
114
|
+
if ssl == 'yes'
|
115
|
+
@mode = :tls
|
116
|
+
else
|
117
|
+
@mode = :original
|
118
|
+
end
|
119
|
+
config['xna'] == 'yes' ? @xna = true : @xna = false
|
120
|
+
config['nzb'] == 'yes' ? @nzb = true : @nzb = false
|
121
|
+
config['header_check'] == 'yes' ? @header_check = true : @header_check = false
|
122
|
+
config['debug'] == 'yes' ? @debug = true : @debug = false
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_msgid(response)
|
126
|
+
msgid = ''
|
127
|
+
response.each do |r|
|
128
|
+
msgid = r.sub(/>.*/, '').tr("<", '') if r.end_with?('Article posted')
|
129
|
+
end
|
130
|
+
return msgid
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_options(args)
|
134
|
+
# version and legal info presented to user
|
135
|
+
banner = []
|
136
|
+
banner << ""
|
137
|
+
banner << "sanguinews v#{Sanguinews::VERSION}. Copyright (c) 2013-2014 Tadeus Dobrovolskij."
|
138
|
+
banner << "Comes with ABSOLUTELY NO WARRANTY. Distributed under GPL v2 license(http://www.gnu.org/licenses/gpl-2.0.txt)."
|
139
|
+
banner << "sanguinews is a simple nntp(usenet) binary poster. It supports multithreading and SSL. More info in README."
|
140
|
+
banner << ""
|
141
|
+
# option parser
|
142
|
+
options = {}
|
143
|
+
options[:filemode] = false
|
144
|
+
options[:files] = []
|
145
|
+
|
146
|
+
opt_parser = OptionParser.new do |opt|
|
147
|
+
opt.banner = "Usage: #{$0} [OPTIONS] [DIRECTORY] | -f FILE1..[FILEX]"
|
148
|
+
opt.separator ""
|
149
|
+
opt.separator "Options"
|
150
|
+
|
151
|
+
opt.on("-c", "--config CONFIG", "use different config file") do |cfg|
|
152
|
+
options[:config] = cfg
|
153
|
+
end
|
154
|
+
opt.on("-C", "--check", "check headers while uploading; slow but reliable") do
|
155
|
+
options[:header_check] = true
|
156
|
+
end
|
157
|
+
opt.on("-f", "--file FILE", "upload FILE, treat all additional parameters as files") do |file|
|
158
|
+
options[:filemode] = true
|
159
|
+
options[:files] << file
|
160
|
+
end
|
161
|
+
opt.on("-g", "--groups GROUP_LIST", "use these groups(comma separated) for upload") do |group_list|
|
162
|
+
options[:groups] = group_list
|
163
|
+
end
|
164
|
+
opt.on("-h", "--help", "help") do
|
165
|
+
banner.each do |msg|
|
166
|
+
puts msg
|
167
|
+
end
|
168
|
+
puts opt_parser
|
169
|
+
puts
|
170
|
+
exit
|
171
|
+
end
|
172
|
+
opt.on("-p", "--password PASSWORD", "use PASSWORD as your password(overwrites config file)") do |password|
|
173
|
+
options[:password] = password
|
174
|
+
end
|
175
|
+
opt.on("-u", "--user USERNAME", "use USERNAME as your username(overwrites config file)") do |username|
|
176
|
+
options[:username] = username
|
177
|
+
end
|
178
|
+
opt.on("-v", "--verbose", "be verbose?") do
|
179
|
+
options[:verbose] = true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
begin
|
184
|
+
opt_parser.parse!(args)
|
185
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
186
|
+
puts opt_parser
|
187
|
+
exit 1
|
188
|
+
end
|
189
|
+
|
190
|
+
options[:directory] = args[0] unless options[:filemode]
|
191
|
+
|
192
|
+
# in file mode treat every additional parameter as a file
|
193
|
+
if !args.empty? && options[:filemode]
|
194
|
+
args.each do |file|
|
195
|
+
options[:files] << file.to_s
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# exit when no file list is provided
|
200
|
+
if options[:directory].nil? && options[:files].empty?
|
201
|
+
puts "You need to specify something to upload!"
|
202
|
+
puts opt_parser
|
203
|
+
exit 1
|
204
|
+
end
|
205
|
+
|
206
|
+
return options
|
207
|
+
end
|
208
|
+
|
209
|
+
def parse_error(msg, **info)
|
210
|
+
if info[:file].nil? || info[:chunk].nil?
|
211
|
+
fileinfo = ''
|
212
|
+
else
|
213
|
+
fileinfo = '(' + info[:file] + ' / Chunk: ' + info[:chunk].to_s + ')'
|
214
|
+
end
|
215
|
+
|
216
|
+
case
|
217
|
+
when /\A411/ === msg
|
218
|
+
@s.log("Invalid newsgroup specified.", stderr: true)
|
219
|
+
when /\A430/ === msg
|
220
|
+
@s.log("No such article. Maybe server is lagging...#{fileinfo}", stderr: true)
|
221
|
+
when /\A(4\d{2}\s)?437/ === msg
|
222
|
+
@s.log("Article rejected by server. Maybe it's too big.#{fileinfo}", stderr: true)
|
223
|
+
when /\A440/ === msg
|
224
|
+
@s.log("Posting not allowed.", stderr: true)
|
225
|
+
when /\A441/ === msg
|
226
|
+
@s.log("Posting failed for some reason.#{fileinfo}", stderr: true)
|
227
|
+
when /\A450/ === msg
|
228
|
+
@s.log("Not authorized.", stderr: true)
|
229
|
+
when /\A452/ === msg
|
230
|
+
@s.log("Wrong username and/or password.", stderr: true)
|
231
|
+
when /\A500/ === msg
|
232
|
+
@s.log("Command not recognized.", stderr: true)
|
233
|
+
when /\A501/ === msg
|
234
|
+
@s.log("Command syntax error.", stderr: true)
|
235
|
+
when /\A502/ === msg
|
236
|
+
@s.log("Access denied.", stderr: true)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def process_and_upload(queue, nntp_pool, info_lock, informed)
|
241
|
+
stuff = queue.pop
|
242
|
+
queue.synchronize do
|
243
|
+
@cond.signal
|
244
|
+
end
|
245
|
+
nntp = nntp_pool.pop
|
246
|
+
|
247
|
+
data = stuff[0]
|
248
|
+
file = stuff[1]
|
249
|
+
msg = data[:message]
|
250
|
+
chunk = data[:chunk]
|
251
|
+
basename = data[:filename]
|
252
|
+
length = data[:length]
|
253
|
+
full_size = msg.length
|
254
|
+
info_lock.synchronize do
|
255
|
+
if !informed[basename.to_sym]
|
256
|
+
@s.log("Uploading #{basename}\n")
|
257
|
+
@s.log(file.subject + "\n")
|
258
|
+
@s.log("Chunks: #{file.chunks}\n", stderr: true) if @verbose
|
259
|
+
informed[basename.to_sym] = true
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
@s.start
|
264
|
+
x = 1
|
265
|
+
begin
|
266
|
+
response = nntp.post msg
|
267
|
+
msgid = get_msgid(response)
|
268
|
+
if @header_check
|
269
|
+
sleep x
|
270
|
+
nntp.stat("<#{msgid}>")
|
271
|
+
end
|
272
|
+
rescue
|
273
|
+
@s.log([$!, $@], stderr: true) if @debug
|
274
|
+
if @verbose
|
275
|
+
parse_error($!.to_s, file: basename, chunk: chunk)
|
276
|
+
@s.log("Upload of chunk #{chunk} from file #{basename} unsuccessful. Retrying...\n", stderr: true)
|
277
|
+
end
|
278
|
+
sleep @delay
|
279
|
+
x += 4
|
280
|
+
retry
|
281
|
+
end
|
282
|
+
|
283
|
+
if @verbose
|
284
|
+
@s.log("Uploaded chunk Nr:#{chunk}\n", stderr: true)
|
285
|
+
end
|
286
|
+
|
287
|
+
@s.done(length)
|
288
|
+
@s.uploaded += full_size
|
289
|
+
if @nzb
|
290
|
+
file.write_segment_info(length, chunk, msgid)
|
291
|
+
end
|
292
|
+
nntp_pool.push(nntp)
|
293
|
+
end
|
294
|
+
|
295
|
+
def run!
|
296
|
+
# Parse options in config file
|
297
|
+
config = "~/.sanguinews.conf"
|
298
|
+
config = File.expand_path(config)
|
299
|
+
# variable to store if config was parsed
|
300
|
+
saw_config = false
|
301
|
+
if File.exist?(config)
|
302
|
+
saw_config = true
|
303
|
+
parse_config(config)
|
304
|
+
end
|
305
|
+
|
306
|
+
options = parse_options(ARGV)
|
307
|
+
|
308
|
+
optconfig = options[:config]
|
309
|
+
optconfig = '' if optconfig.nil?
|
310
|
+
if !File.exist?(optconfig) && !saw_config
|
311
|
+
puts "No config information specified. Aborting..."
|
312
|
+
exit
|
313
|
+
end
|
314
|
+
parse_config(optconfig) if File.exist?(optconfig)
|
315
|
+
|
316
|
+
options[:verbose] ? @verbose = true : @verbose = false
|
317
|
+
@header_check = true unless options[:header_check].nil?
|
318
|
+
filemode = options[:filemode]
|
319
|
+
|
320
|
+
@username = options[:username] unless options[:username].nil?
|
321
|
+
@password = options[:password] unless options[:password].nil?
|
322
|
+
@groups = options[:groups] unless options[:groups].nil?
|
323
|
+
directory = options[:directory] unless filemode
|
324
|
+
files = options[:files]
|
325
|
+
|
326
|
+
# skip hidden files
|
327
|
+
if !filemode
|
328
|
+
directory = directory + "/" unless directory.end_with?('/')
|
329
|
+
Dir.foreach(directory) do |item|
|
330
|
+
next if item.start_with?('.')
|
331
|
+
files << directory+item
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# "max" is needed only in dirmode
|
336
|
+
max = files.length
|
337
|
+
c = 1
|
338
|
+
|
339
|
+
unprocessed = 0
|
340
|
+
info_lock=Mutex.new
|
341
|
+
messages = Queue.new
|
342
|
+
messages.extend(MonitorMixin)
|
343
|
+
@cond = messages.new_cond
|
344
|
+
files_to_process = []
|
345
|
+
@s = Speedometer.new(units: "KB", progressbar: true)
|
346
|
+
@s.uploaded = 0
|
347
|
+
|
348
|
+
pool = Queue.new
|
349
|
+
Thread.new {
|
350
|
+
@threads.times do |x|
|
351
|
+
nntp = connect(x)
|
352
|
+
pool.push(nntp)
|
353
|
+
end
|
354
|
+
}
|
355
|
+
|
356
|
+
p = Pool.new(@threads)
|
357
|
+
informed = {}
|
358
|
+
|
359
|
+
files.each do |file|
|
360
|
+
next if !File.file?(file)
|
361
|
+
|
362
|
+
informed[file.to_sym] = false
|
363
|
+
file = FileToUpload.new(
|
364
|
+
name: file, chunk_length: @length,
|
365
|
+
prefix: @prefix, current: c, last: max, filemode: filemode,
|
366
|
+
from: @from, groups: @groups, nzb: @nzb
|
367
|
+
)
|
368
|
+
@s.to_upload += file.size
|
369
|
+
|
370
|
+
info_lock.synchronize do
|
371
|
+
unprocessed += file.chunks
|
372
|
+
end
|
373
|
+
|
374
|
+
files_to_process << file
|
375
|
+
c += 1
|
376
|
+
end
|
377
|
+
|
378
|
+
# let's give a little bit higher priority for file processing thread
|
379
|
+
@t = Thread.new {
|
380
|
+
files_to_process.each do |file|
|
381
|
+
@s.log("Calculating CRC32 value for #{file.name}\n", stderr: true) if @verbose
|
382
|
+
file.file_crc32
|
383
|
+
@s.log("Encoding #{file.name}\n")
|
384
|
+
yencode(file, @length, messages)
|
385
|
+
end
|
386
|
+
}
|
387
|
+
@t.priority += 2
|
388
|
+
|
389
|
+
until unprocessed == 0
|
390
|
+
p.schedule do
|
391
|
+
process_and_upload(messages, pool, info_lock, informed)
|
392
|
+
end
|
393
|
+
unprocessed -= 1
|
394
|
+
end
|
395
|
+
|
396
|
+
p.shutdown
|
397
|
+
|
398
|
+
until pool.empty?
|
399
|
+
nntp = pool.pop
|
400
|
+
nntp.finish
|
401
|
+
end
|
402
|
+
|
403
|
+
@s.stop
|
404
|
+
puts
|
405
|
+
|
406
|
+
files_to_process.each do |file|
|
407
|
+
if files_to_process.last == file
|
408
|
+
last = true
|
409
|
+
else
|
410
|
+
last = false
|
411
|
+
end
|
412
|
+
file.close(last)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|