ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-FCP/bin/fmd
Revision: 1.16
Committed: Mon Dec 4 13:47:28 2006 UTC (19 years, 9 months ago) by root
Branch: MAIN
Changes since 1.15: +6 -8 lines
Log Message:
*** empty log message ***

File Contents

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