ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-FCP/bin/fmd
Revision: 1.9
Committed: Mon Nov 20 19:24:16 2006 UTC (19 years, 9 months ago) by root
Branch: MAIN
Changes since 1.8: +21 -13 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 #!/opt/bin/perl
2
3 =head1 fmd - the freenet mass downloader
4
5 Fmd is at a very early stage of development (and very hackish, too), as I
6 am learning the basics of freenet myself.
7
8 However, I use it in production, and since it verifies everything it
9 decodes etc., it seems to be quite safe to use, that is, if you know how
10 to debug perl :)
11
12 =head2 FEATURES
13
14 - decoding is done "in place", i.e. all non-checkblock blocks are
15 stored in-place and will not be moved, only checkblocks will
16 be moved to their final position in the file before decoding.
17 - high resistance against failures of fred or fmd
18 - extremely persistent retry behaviour - there is no such thing
19 as a permanent failure.
20 - handles hundreds of simultaneous downloads with grace.
21
22 =head2 ENVIRONMENT
23
24 Set FMD_HOME to a directory (default ~/fmd) where fmd will store it's
25 files. It will not store your porn files or other freenet data outside
26 that directory.
27
28 The subdirectory db will contain a database (soon to go away), while tmp
29 contains queue files and partial splitfiles. All finished files will be
30 moved to the done subdir.
31
32 FREDHOST and FREDPORT do the obvious. Fix these docs if you disagree.
33
34 Also edit the fmd executable for the number of threads and other
35 non-useful constants. The default number (200) works for me, probably not
36 for you.
37
38 =head2 COMMANDS
39
40 =over 4
41
42 =item CHK@... (space or "/") filename
43
44 Just pasting a CHK and a filename sperated by one or more spaces or
45 slashes will add files to the queue. The line can have leading garbage,
46 i.e. you can paste full uris.
47
48 =item <attachment>...</attachment>
49
50 =item <attach>...</attach>
51
52 =item <attached>...</attached>
53
54 Will add frosts mentally deranged pseudo-xml format, soon to be replaced
55 by something even more horrible.
56
57 =item l
58
59 List all jobs by number.
60
61 =item <number> (optionally trailing command)
62
63 Selects the job with the given number for further comamnds that require a current job.
64
65 =item s
66
67 Show the current job. Bot useful right now.
68
69 =item k
70
71 Kill the current job. This will keep the temp. file around. Sorry. (But it
72 could be used to reconstruct the download.. hmm..)
73
74 =item pri<number>
75
76 Set the current job priority to <pri>. The default is 1. A job with higher
77 priority will (on average) get more requests.
78
79 The useful range is probably 0..100, but be careful and limit yourself to
80 small values (<10), otherwise your other downloads might starve!
81
82 =item q
83
84 Kill the command prompt. Yupp.
85
86 =back
87
88 =cut
89
90 use Net::FCP;
91 use Storable;
92 use Time::HiRes;
93 use Coro;
94 use Coro::Event;
95 use Coro::Handle;
96 use Coro::Signal;
97 use Coro::Timer;
98 use Coro::AIO;
99 use List::Util;
100 use Digest::SHA1;
101 use Algorithm::FEC;
102 use Digest::SHA1;
103 use Net::FCP::Util;
104 use POSIX ();
105
106 Coro::AIO::max_poll_time 0.01;
107 Coro::AIO::min_parallel 1;
108 Coro::AIO::max_parallel 2;
109
110 $|=1;
111
112 our $MAX_TXN = 150; # use max. this many transactions in parallel
113 our @HTL = (5,12,20);
114 our $VERIFY_CHK = 1; # verify all blocks, again and again (only useful to debugging).
115 our $FMD_HOME = $ENV{FMD_HOME} || "$ENV{HOME}/fmd";
116
117 our $FCP = new Net::FCP;
118
119 defined $FMD_HOME
120 or die "you currently must define FMD_HOME to a persistent directory";
121
122 mkdir $FMD_HOME, 0700;
123
124 our $QUEUE_HOME = "$FMD_HOME/tmp";
125 mkdir $QUEUE_HOME, 0700;
126 our $DONE_HOME = "$FMD_HOME/done";
127 mkdir $DONE_HOME, 0700;
128
129 our %job;
130
131 sub push_key {
132 my ($key, $title) = @_;
133
134 for my $job (values %job) {
135 if ($job->{p}{key} eq $key) {
136 warn "job $job->{id} already works on this, not adding";
137 return;
138 }
139 }
140
141 my $job = job->new_from_key ($key, $title);
142 warn "added as job $job->{id}\n";
143 $job;
144 }
145
146 sub cmdline {
147 my ($i, $o) = @_;
148
149 my $cmd = async {
150 my $job;
151 while (print $o "> " and defined ($_ = <$i>)) {
152 chomp;
153 if (/<attach(?:ment|ed|)>(.*) \* (CHK[^<]+)<\/attach/) {
154 $job = push_key $2, $1;
155 } elsif (/(CHK\@[a-zA-Z0-9,~\-]{54})[\/ ]+(.*?)\s*$/) {
156 $job = push_key $1, $2;
157 } elsif (s/^(\d+)//) {
158 if ($job = $job{$1}) {
159 print $o delete $job->{log};
160 if ($job->{input}) {
161 }
162 }
163 redo;
164 } elsif (/^q/) {
165 close $i;
166 close $o;
167 } elsif (/^l/) {
168 for my $job (sort { $a->{id} <=> $b->{id} } values %job) {
169 print $o "$_ $job->{id}: $job->{p}{key} $job->{p}{title} $job->{status}\n";
170 }
171 } elsif (/^pri\s*(\d+)/) {
172 if ($job) {
173 $job->{p}{pri} = $1;
174 $job->save;
175 }
176 } elsif (/^s$/) {
177 print $o $job->show if $job;
178 } elsif (/^k$/) {
179 $job->kill if $job;
180 } elsif (/\S/) {
181 #
182 } else {
183 print $o "?\n";
184 }
185 for my $job (sort { $a->{id} <=> $b->{id} } values %job) {
186 if ($job->{_input}) {
187 print $o "> $job->{id} $job->{title} $job->{status}\n";
188 }
189 }
190 }
191 };
192 }
193
194 package job;
195
196 use Coro;
197 use Coro::AIO;
198 use Fcntl;
199 use IO::Handle;
200 use Array::Heap2;
201
202 my $count = 0;
203
204 sub new {
205 my $class = shift;
206
207 my $self = bless { @_ }, $class;
208
209 $self->{p}{title} !~ /\//
210 or die "$self->{p}{title}: malformed key\n";
211
212 $self->{id} = ++$count;
213 $self->{job} ||= "$QUEUE_HOME/" . Time::HiRes::time . ":$count.j";
214
215 $job{$self->{id}} = $self;
216
217 $self->save;
218 $self->start;
219 $self;
220 }
221
222 sub new_from_key {
223 my ($class, $key, $title) = @_;
224 $class->new (p => { key => $key, title => $title, state => "examine" });
225 }
226
227 sub new_from_file {
228 my ($class, $path) = @_;
229 $class->new (job => $path, p => Storable::retrieve $path);
230 }
231
232 sub save {
233 my ($self) = @_;
234 Storable::nstore $self->{p}, "$self->{job}~";
235 aio_rename "$self->{job}~", $self->{job};
236 }
237
238 sub clean {
239 my ($self) = @_;
240
241 delete $job{$self->{id}};
242 $self->save;
243 aio_rename $self->{job}, "$DONE_HOME/$self->{p}{title}.job";
244 aio_unlink $self->{job};
245 }
246
247 sub kill {
248 my ($self) = @_;
249
250 $self->clean;
251 $self->{coro}->cancel;
252 }
253
254 my @queue;
255 my $queue_change = new Coro::Signal;
256 my $queue_alloc = 0;
257
258 async {
259 for (;;) {
260 while (@queue
261 and (($queue[0][0] > 10 and $queue_alloc < $MAX_TXN)
262 or ($queue[0][0] > 1 and $queue_alloc < $MAX_TXN - 3)
263 or $queue_alloc < $MAX_TXN - 5)) {
264 (pop_heap @queue)->[1]->send;
265 $queue_alloc++;
266 Coro::Timer::sleep 0.05;
267 }
268 $queue_change->wait;
269 }
270 };
271
272 sub txn_begin {
273 my ($pri) = @_;
274 my $sig = new Coro::Signal;
275
276 #warn "txn_begin $pri\n";#d#
277 push_heap @queue, [$pri, $sig];
278 $queue_change->send;
279 $sig->wait;
280 }
281
282 sub txn_end {
283 $queue_alloc--;
284 $queue_change->send;
285 }
286
287 sub fetch_uri {
288 my ($pri, $uri) = @_;
289
290 for(my $count = 1; ; $count += 0.3) {
291 for my $htl (@HTL) {
292 txn_begin time + $htl + $count;
293 my $sig = new Coro::Signal;
294 my ($meta, $data) = eval { @{ $FCP->client_get ($uri, $htl) } };
295 txn_end;
296 if ($@) {
297 if (UNIVERSAL::isa ($@, Net::FCP::Exception::)) {
298 if ($@->type ("data_not_found")
299 || $@->type ("route_not_found")) {
300 next;
301 }
302 if ($@->type ("short_data")) {
303 warn "(short_data, redo)\n";
304 redo;
305 }
306 die;
307 }
308 }
309 if (defined $data) {
310 return ($meta, $data);
311 }
312 }
313 }
314
315 die;
316 }
317
318 sub log {
319 my ($self, $text) = @_;
320 my $time = POSIX::strftime "%H:%M:%S", localtime time;
321 warn "$time $self->{id},$self->{p}{pri}: $text\n";
322 $self->{log} .= "$time $text\n";
323 }
324
325 sub feedback {
326 my ($self, $prompt) = @_;
327 $self->{input} = [$Coro::current, $prompt];
328 Coro::schedule;
329 }
330
331 sub show {
332 my ($self) = @_;
333
334 "ID: $self->{id}\n"
335 . "Title: $self->{p}{title}\n"
336 . "Blocks#: " . @{$self->{p}{blk}} . "\n"
337 . "Blocks: " . (join "", map {
338 $_->{done} ? "+" : "-"
339 } @{$self->{p}{blk}}) . "\n" .
340 "";
341 }
342
343 my $id;
344
345 sub MAXSEG (){ 128*1024*1024 }
346 sub MINSEG (){ 6* 128*1024 }
347
348 sub blocksize($) {
349 return
350 $_[0] >= 64*1024*1024 ? 1024*1024
351 : $_[0] >= 32*1024*1024 ? 512*1024
352 : $_[0] >= 1024*1024 ? 256*1024
353 : 128*1024;
354 }
355
356 sub start {
357 my ($self) = @_;
358
359 $self->{p}{pri} ||= 1;
360 $self->{p}{state} ||= "examine";
361
362 $self->{job} =~ /^(.*)\.j$/ or die "$self->{job}: missing .j";
363 $self->{file} = "$1.d";
364 $self->{fh} = aio_open $self->{file}, O_RDWR|O_CREAT, 0600
365 or die "$self->{file}: $!";
366
367 $self->{status} = "starting";
368 $self->{coro} = async {
369 for(;;) {
370 my ($state, @args) = ref $self->{p}{state} ? @{$self->{p}{state}} : $self->{p}{state};
371 my $next = eval { $self->can ("state_$state")->($self, @args) };
372 if ($@) {
373 $self->log ($@);
374 $next = $self->feedback ("continue with state: ");
375 }
376 $self->log ($self->{status} = "STATE CHANGE: $next");
377 $self->{p}{state} = $next;
378 $self->save;
379 }
380 };
381 }
382
383 sub state_finish {
384 my ($self, $save) = @_;
385
386 if ($save) {
387 IO::Handle::sync $self->{fh};
388 close $self->{fh};
389
390 unlink "$DONE_HOME/$self->{p}{title}";
391 link $self->{file}, "$DONE_HOME/$self->{p}{title}"
392 or die "link: $self->{file} => $DONE_HOME/$self->{p}{title}: $!";
393 system "sync";
394 }
395 $self->clean;
396
397 unlink $self->{file};
398
399 $self->{status} = "finished";
400 $self->feedback ("finished");
401 terminate;
402 }
403
404 sub state_examine {
405 my ($self) = @_;
406 my $p = $self->{p};
407
408 $self->{status} = "initial fetch";
409
410 for (;;) {
411 $self->log ("fetching $p->{key} (=$p->{title})");
412
413 ($p->{meta}, $p->{data}) = fetch_uri 100, "freenet:$p->{key}";
414 $self->save;
415 #use PApp::Util; print STDERR PApp::Util::dumpval [keys %{$meta->{document}[0]{split_file}}];
416 $self->log ("type $p->{meta}{document}[0]{info}{format}");
417
418 if (my $splitfile = $p->{meta}{document}[0]{split_file}) {
419 return "splitfile";
420 } elsif ((defined $p->{data}) and (length $p->{data})) {
421 syswrite $self->{fh}, delete $p->{data};
422 return ["finish", 1];
423 }
424
425 $self->log ("EMPTY, retrying in an hour");
426 Coro::Timer::sleep 3600;
427 }
428
429 }
430
431 sub state_splitfile {
432 my ($self) = @_;
433 my $p = $self->{p};
434
435 my $splitfile = $p->{meta}{document}[0]{split_file};
436 my $filesize = hex $splitfile->{size};
437
438 if ($splitfile->{algo_name} eq "OnionFEC_a_1_2") {
439 my $data_packets = hex $splitfile->{block_count};
440 my $check_packets = hex $splitfile->{check_block_count};
441
442 my $blk = ($p->{blk} ||= []);
443
444 unless (@$blk) {
445 for (1..$data_packets) {
446 push @$blk, {
447 uri => $splitfile->{block}{sprintf "%x", $_},
448 };
449 }
450 for (1..$check_packets) {
451 push @$blk, {
452 uri => $splitfile->{check_block}{sprintf "%x", $_},
453 };
454 }
455 }
456
457 my @segments;
458 my $segments = 0;
459
460 {
461 # that is a horrible algorithm :(, these freenet freaks are... java-disabled
462 # hardcoding lots of magic parameters is soo dumb.
463 my $size = $filesize;
464 my $offset = 0;
465 my $offset2 = ($filesize & ~(1024*1024-1)) + 1024*1024; # leave enough space after last data block
466 my $idx = 0;
467 my $idx2 = $data_packets;
468 my @redundandy = (0,1,2); # maybe OnionFAC_a_1_2 means 1/2 redundancy(?)
469
470 while ($size > 0) {
471 my $segsize = $size >= MAXSEG ? MAXSEG : $size <= MINSEG ? MINSEG : $size;
472 my $blksize = blocksize $segsize;
473 my $seg =
474 {
475 id => $segments++,
476 todo => int (($segsize + $blksize - 1) / $blksize),
477 done => 0,
478 blk => [],
479 blksize => $blksize,
480 };
481
482 push @segments, $seg;
483 $size -= $segsize;
484
485 while ($segsize > 0) {
486 push @{$seg->{blk}}, $idx;
487 for ($blk->[$idx++]) {
488 $_->{offset} = $offset;
489 #$_->{size} = $blksize > $segsize ? $segsize : $blksize; # WRONG
490 $_->{size} = $blksize;
491 $_->{seg} = $seg;
492 }
493
494 $segsize -= $blksize;
495 $offset += $blksize;
496
497 if (($redundandy[0] += $redundandy[1]) >= $redundandy[2]) {
498 $redundandy[0] -= $redundandy[2];
499
500 push @{$seg->{blk}}, $idx2;
501 for ($blk->[$idx2++]) {
502 $_->{offset} = $offset2;
503 $_->{size} = $blksize;
504 $_->{seg} = $seg;
505 }
506
507 $offset2 += $blksize;
508 }
509 }
510 }
511
512 $idx == $data_packets
513 or die "$self->{id}/$p->{tile} $self->{job}\nidx $idx != data_packets $data_packets";
514 $idx2 == $data_packets + $check_packets
515 or die "$self->{id}/$p->{tile} $self->{job}\nidx2 $idx2 != data_packets $data_packets + check_packets $check_packets";
516 }
517
518 for (@$blk) {
519 ++$_->{seg}{done} if $_->{done};
520 delete $_->{htl};
521 }
522
523 my $fail = 0;
524 my $sig = new Coro::Signal;
525
526 $self->{status} = "splitfile fetch (" . @$blk . " blocks)";
527
528 my @txn;
529
530 for (;;) {
531 for my $id (0 .. $#$blk) {
532 my $blk = $blk->[$id];
533
534 next if $txn[$id] || $blk->{done} || $blk->{seg}{todo} <= $blk->{seg}{done};
535
536 my $htl = $HTL[$blk->{htl}++ % @HTL];
537 my $pri = int time + $htl / 20 * 7200 * rand;
538
539 txn_begin $pri;
540 #warn $self->{id} . ", GET<$htl, $pri>\n";#d#
541 $txn[$id] ||= $FCP->txn_client_get ($blk->{uri}, $htl)->cb(sub {
542 undef $txn[$id];
543
544 my $seg = $blk->{seg};
545
546 my ($meta, $data) = eval { @{ $_[0]->result } };
547
548 if (defined $data) {
549 $blk->{size} == length $data
550 or die sprintf "block $id expected size %d, got %d\n", $blk->{size}, length $data;
551
552 (length $data) == (aio_write $self->{fh}, $blk->{offset}, (length $data), $data, 0)
553 or die "unable to write chunk to disk, not setting valid flag";
554 aio_fsync $self->{fh};
555
556 $blk->{done} = 1;
557 $blk->{meta} = $meta->{raw} if length $meta->{raw};#d#
558 $seg->{done}++;
559 $self->save;
560
561 $::htl_sum += $htl;
562 $::htl_cnt++;
563
564 $self->log (sprintf "got block $seg->{id}.$id %d ($seg->{done}/$seg->{todo}) at htl $htl (%f) and pri $pri",
565 length $data, $::htl_sum / $::htl_cnt);
566 } else {
567 if ($@) {
568 if ($@->type ("data_not_found")) {
569 # nop
570 } elsif ($@->type ("network_error")) {
571 $self->log ("$@, retrying in 1s");
572 CORE::sleep 1;
573 } else {
574 $self->log ("$@");
575 }
576 }
577 ++$fail;
578 }
579 $self->{status} = "splitfile fetch ($seg->{done}/$seg->{todo}, $fail failed)";
580
581 txn_end;
582 $sig->send;
583 });
584 }
585
586 for my $seg (@segments) {
587 if ($seg->{done} >= $seg->{todo} && !$seg->{finished}) {
588
589 $self->log ("segment done, cancelling segment $seg->{id}");
590 for my $id (@{$seg->{blk}}) {
591 (delete $txn[$id])->cancel if $txn[$id];
592 }
593
594 $self->log ("verifying segment $seg->{id}");
595 for my $id (@{$seg->{blk}}) {
596 my $blk = $blk->[$id];
597 if ($blk->{done}) {
598 sysseek $self->{fh}, $blk->{offset}, 0;
599 sysread $self->{fh}, my $buf, $blk->{size};
600
601 my $k1 = Net::FCP::Util::extract_chk_hash $blk->{uri};
602 my $k2 = Net::FCP::Util::generate_chk_hash $blk->{meta}, $buf;
603
604 if ($k1 ne $k2) {
605 print "v";#d#
606 #warn sprintf "$p->{title} block $id BROKEN (%s != %s)", (unpack "H*", $k1), (unpack "H*", $k2);
607 $blk->{done} = 0;
608 $seg->{done}--;
609 $self->save;
610 } else {
611 print "V";#d#
612 }
613 }
614 }
615 print "\n";
616
617 if ($seg->{done} >= $seg->{todo} && !$seg->{finished}) {
618 $self->log ("verified segment OK $seg->{id}");
619 $seg->{finished}++;
620 $segments--;
621 } else {
622 $self->log ("verified segment NOT OK $seg->{id}");
623 }
624 }
625 }
626
627 last unless $segments;
628
629 $sig->wait;
630 }
631
632 $self->log ("decoding < $self->{job} $self->{file} $filesize >");
633
634 for my $seg (@segments) {
635 my @part;
636 my @idx;
637 my @blk = map $blk->[$_], sort { $a <=> $b } @{$seg->{blk}};
638
639 for my $id (0 .. $#blk) {
640 my $blk = $blk[$id];
641 next unless $blk->{done};
642
643 push @part, [$self->{fh}, $blk->{offset}];
644 push @idx, $id;
645
646 last if @idx == $seg->{todo};
647 }
648
649 my $fec = new Algorithm::FEC
650 $seg->{todo},
651 scalar @blk,
652 $seg->{blksize};
653
654 $fec->shuffle (\@part, \@idx);
655
656 # now copy check blocks to their destination position
657 for my $i (0 .. $#idx) {
658 next if $idx[$i] == $i;
659
660 my $src = $part[$i];
661 $part[$i] = [$self->{fh}, $blk[$i]{offset}];
662 $fec->copy ($src, $part[$i]);
663 }
664
665 $fec->set_decode_blocks (\@part, \@idx);
666 $fec->decode;
667 }
668
669 my $sha1 = new Digest::SHA1;
670 open my $dd, "-|", "head -c$filesize \Q$self->{file}\E"
671 or die "DD: $!";
672 #$dd = Coro::Handle::unblock $dd;
673 $sha1->addfile ($dd);
674 $sha1 = $sha1->hexdigest;
675
676 if (exists $p->{meta}{document}[0]{info}{checksum}
677 and $p->{meta}{document}[0]{info}{checksum} ne $sha1) {
678 $self->log ("META: $p->{meta}{document}[0]{info}{checksum} and real checksum $sha1 for $filesize DIFFER");
679 # $self->feedback ("CHECKSUM ERROR");
680 # terminate;
681 }
682
683 truncate $self->{fh}, $filesize;
684 sysseek $self->{fh}, 0, 0;
685
686 return ["finish", 1];
687 } else {
688 $self->log ("splitfile algo '$splitfile->{algo_name}' unknown");
689 $self->feedback ("algo unknown");
690 terminate;
691 }
692 }
693
694 package main;
695
696 $|=1;
697
698 for (<\Q$QUEUE_HOME\E/*.j>) {
699 job->new_from_file ($_);
700 print "J";
701 }
702 print "\n";
703
704 open my $stdin , "<&0" or die;
705 open my $stdout, ">&1" or die;
706 cmdline unblock $stdin, unblock $stdout;
707
708 &Coro::Event::loop;
709