ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-FCP/bin/fmd
Revision: 1.15
Committed: Fri Dec 1 03:48:36 2006 UTC (19 years, 9 months ago) by root
Branch: MAIN
Changes since 1.14: +17 -6 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 +(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 print "+";#d#
289 #warn "txn_begin $pri\n";#d#
290 push_heap @queue, [$pri, $sig];
291 $queue_change->send;
292 $sig->wait;
293 }
294
295 sub txn_end {
296 $queue_alloc--;
297 $queue_change->send;
298 print "-";#d#
299 }
300
301 sub txn_client_get {
302 my %arg = @_;
303
304 txn_begin $arg{pri};
305 $FCP->txn_client_get ($arg{uri}, $arg{htl})->cb (unblock_sub {
306 txn_end;
307
308 $arg{cb}->(@_);
309 });
310 }
311
312 sub fetch_uri {
313 my ($pri, $uri) = @_;
314
315 for(my $count = 1; ; $count += 0.3) {
316 for my $htl (@HTL) {
317 txn_begin time + $htl + $count;
318 my ($meta, $data) = eval { @{ $FCP->client_get ($uri, $htl) } };
319 txn_end;
320 if ($@) {
321 if (UNIVERSAL::isa ($@, Net::FCP::Exception::)) {
322 if ($@->type ("data_not_found")
323 || $@->type ("route_not_found")) {
324 next;
325 }
326 if ($@->type ("short_data")) {
327 warn "(short_data, redo)\n";
328 redo;
329 }
330 die;
331 }
332 }
333 if (defined $data) {
334 return ($meta, $data);
335 }
336 }
337 }
338
339 die;
340 }
341
342 sub log {
343 my ($self, $text) = @_;
344 my $time = POSIX::strftime "%H:%M:%S", localtime time;
345 warn "$time $self->{id},$self->{p}{pri}: $text\n";
346 $self->{log} .= "$time $text\n";
347 }
348
349 sub feedback {
350 my ($self, $prompt) = @_;
351 $self->{input} = [$Coro::current, $prompt];
352 Coro::schedule;
353 }
354
355 sub show {
356 my ($self) = @_;
357
358 "ID: $self->{id}\n"
359 . "Title: $self->{p}{title}\n"
360 . "Blocks#: " . @{$self->{p}{blk}} . "\n"
361 . "Blocks: " . (join "", map {
362 $_->{done} ? "+" : "-"
363 } @{$self->{p}{blk}}) . "\n" .
364 ""
365 }
366
367 our $id;
368
369 sub MAXSEG (){ 128*1024*1024 }
370 sub MINSEG (){ 6* 128*1024 }
371
372 sub blocksize($) {
373 return
374 $_[0] >= 64*1024*1024 ? 1024*1024
375 : $_[0] >= 32*1024*1024 ? 512*1024
376 : $_[0] >= 1024*1024 ? 256*1024
377 : 128*1024;
378 }
379
380 sub start {
381 my ($self) = @_;
382
383 $self->{p}{pri} ||= 1;
384 $self->{p}{state} ||= "examine";
385
386 $self->{job} =~ /^(.*)\.j$/ or die "$self->{job}: missing .j";
387 $self->{file} = "$1.d";
388 $self->{fh} = aio_open $self->{file}, O_RDWR|O_CREAT, 0600
389 or die "$self->{file}: $!";
390
391 $self->{status} = "starting";
392 $self->{coro} = async {
393 for(;;) {
394 my ($state, @args) = ref $self->{p}{state} ? @{$self->{p}{state}} : $self->{p}{state};
395 my $next = eval { $self->can ("state_$state")->($self, @args) };
396 if ($@) {
397 $self->log ($@);
398 $next = $self->feedback ("continue with state: ");
399 }
400 $self->log ($self->{status} = "STATE CHANGE: ". join ", ", ref $next ? @$next : $next);
401 $self->{p}{state} = $next;
402 $self->save;
403 }
404 };
405 }
406
407 sub state_finish {
408 my ($self, $save) = @_;
409
410 if ($save) {
411 aio_fsync $self->{fh};
412 close $self->{fh};
413
414 aio_unlink "$DONE_HOME/$self->{p}{title}";
415 aio_link $self->{file}, "$DONE_HOME/$self->{p}{title}"
416 and die "link: $self->{file} => $DONE_HOME/$self->{p}{title}: $!";
417 system "sync";
418 }
419 $self->clean;
420
421 aio_unlink $self->{file};
422
423 $self->{status} = "finished";
424 $self->feedback ("finished");
425 terminate;
426 }
427
428 sub state_examine {
429 my ($self) = @_;
430 my $p = $self->{p};
431
432 $self->{status} = "initial fetch";
433
434 for (;;) {
435 $self->log ("fetching $p->{key} (=$p->{title})");
436
437 ($p->{meta}, $p->{data}) = fetch_uri 100, "freenet:$p->{key}";
438 $self->save;
439 #use PApp::Util; print STDERR PApp::Util::dumpval [keys %{$meta->{document}[0]{split_file}}];
440 $self->log ("type $p->{meta}{document}[0]{info}{format}");
441
442 if (my $splitfile = $p->{meta}{document}[0]{split_file}) {
443 return "splitfile";
444 } elsif ((defined $p->{data}) and (length $p->{data})) {
445 syswrite $self->{fh}, delete $p->{data};
446 aio_fsync $self->{fh};
447 return ["finish", 1];
448 }
449
450 $self->log ("EMPTY, retrying in an hour");
451 Coro::Timer::sleep 3600;
452 }
453
454 }
455
456 sub state_splitfile {
457 my ($self) = @_;
458 my $p = $self->{p};
459
460 my $splitfile = $p->{meta}{document}[0]{split_file};
461 my $filesize = hex $splitfile->{size};
462
463 if ($splitfile->{algo_name} eq "OnionFEC_a_1_2") {
464 my $data_packets = hex $splitfile->{block_count};
465 my $check_packets = hex $splitfile->{check_block_count};
466
467 my $blk = ($p->{blk} ||= []);
468
469 unless (@$blk) {
470 for (1..$data_packets) {
471 push @$blk, {
472 uri => $splitfile->{block}{sprintf "%x", $_},
473 };
474 }
475 for (1..$check_packets) {
476 push @$blk, {
477 uri => $splitfile->{check_block}{sprintf "%x", $_},
478 };
479 }
480 }
481
482 my @segments;
483 my $segments = 0;
484
485 {
486 # that is a horrible algorithm :(, these freenet freaks are... java-disabled
487 # hardcoding lots of magic parameters is soo dumb.
488 my $size = $filesize;
489 my $offset = 0;
490 my $offset2 = ($filesize & ~(1024*1024-1)) + 1024*1024; # leave enough space after last data block
491 my $idx = 0;
492 my $idx2 = $data_packets;
493 my @redundandy = (0,1,2); # maybe OnionFAC_a_1_2 means 1/2 redundancy(?)
494
495 while ($size > 0) {
496 my $segsize = $size >= MAXSEG ? MAXSEG : $size <= MINSEG ? MINSEG : $size;
497 my $blksize = blocksize $segsize;
498 my $seg =
499 {
500 id => $segments++,
501 todo => int (($segsize + $blksize - 1) / $blksize),
502 done => 0,
503 blk => [],
504 blksize => $blksize,
505 };
506
507 push @segments, $seg;
508 $size -= $segsize;
509
510 while ($segsize > 0) {
511 push @{$seg->{blk}}, $idx;
512 for ($blk->[$idx++]) {
513 $_->{offset} = $offset;
514 #$_->{size} = $blksize > $segsize ? $segsize : $blksize; # WRONG
515 $_->{size} = $blksize;
516 $_->{seg} = $seg;
517 }
518
519 $segsize -= $blksize;
520 $offset += $blksize;
521
522 if (($redundandy[0] += $redundandy[1]) >= $redundandy[2]) {
523 $redundandy[0] -= $redundandy[2];
524
525 push @{$seg->{blk}}, $idx2;
526 for ($blk->[$idx2++]) {
527 $_->{offset} = $offset2;
528 $_->{size} = $blksize;
529 $_->{seg} = $seg;
530 }
531
532 $offset2 += $blksize;
533 }
534 }
535 }
536
537 $idx == $data_packets
538 or die "$self->{id}/$p->{tile} $self->{job}\nidx $idx != data_packets $data_packets";
539 $idx2 == $data_packets + $check_packets
540 or die "$self->{id}/$p->{tile} $self->{job}\nidx2 $idx2 != data_packets $data_packets + check_packets $check_packets";
541 }
542
543 for (@$blk) {
544 ++$_->{seg}{done} if $_->{done};
545 delete $_->{htl};
546 }
547
548 my $fail = 0;
549 my $sig = new Coro::Signal;
550
551 $self->{status} = "splitfile fetch (" . @$blk . " blocks)";
552
553 my @txn;
554
555 for (;;) {
556 for my $id (0 .. $#$blk) {
557 my $blk = $blk->[$id];
558
559 next if $txn[$id] || $blk->{done} || $blk->{seg}{todo} <= $blk->{seg}{done};
560
561 my $htl = $HTL[$blk->{htl}++ % @HTL];
562 my $pri = int time + $htl / 20 * 7200 * rand;
563
564 #warn $self->{id} . ", GET<$htl, $pri>\n";#d#
565 $txn[$id] ||= txn_client_get pri => $pri, uri => $blk->{uri}, htl => $htl, cb => sub {
566 undef $txn[$id];
567
568 my $seg = $blk->{seg};
569
570 my ($meta, $data) = eval { @{ $_[0]->result } };
571
572 if (defined $data) {
573 $blk->{size} == length $data
574 or die sprintf "block $id expected size %d, got %d\n", $blk->{size}, length $data;
575
576 (length $data) == (aio_write $self->{fh}, $blk->{offset}, (length $data), $data, 0)
577 or die "unable to write chunk to disk, not setting valid flag";
578 aio_fsync $self->{fh};
579
580 $blk->{done} = 1;
581 $blk->{meta} = $meta->{raw} if length $meta->{raw};#d#
582 $seg->{done}++;
583 $self->save;
584
585 $::htl_sum += $htl;
586 $::htl_cnt++;
587
588 $self->log (sprintf "got block $seg->{id}.$id %d ($seg->{done}/$seg->{todo}) at htl $htl (%f) and pri $pri",
589 length $data, $::htl_sum / $::htl_cnt);
590 } else {
591 if ($@) {
592 if ($@->type ("data_not_found")) {
593 # nop
594 } elsif ($@->type ("network_error")) {
595 $self->log ("$@, retrying in 1s");
596 CORE::sleep 1;
597 } else {
598 $self->log ("$@");
599 }
600 }
601 ++$fail;
602 }
603 $self->{status} = "splitfile fetch ($seg->{done}/$seg->{todo}, $fail failed)";
604
605 $sig->send;
606 };
607 }
608
609 for my $seg (@segments) {
610 if ($seg->{done} >= $seg->{todo} && !$seg->{finished}) {
611
612 $self->log ("segment done, cancelling segment $seg->{id}");
613 for my $id (@{$seg->{blk}}) {
614 (delete $txn[$id])->cancel if $txn[$id];
615 }
616
617 my $verify;
618 for my $id (@{$seg->{blk}}) {
619 my $blk = $blk->[$id];
620 if ($blk->{done}) {
621 aio_read $self->{fh}, $blk->{offset}, $blk->{size}, my $buf, 0;
622
623 my $k1 = Net::FCP::Util::extract_chk_hash $blk->{uri};
624 my $k2 = Net::FCP::Util::generate_chk_hash $blk->{meta}, $buf;
625
626 if ($k1 ne $k2) {
627 $verify .= "v";
628 #warn sprintf "$p->{title} block $id BROKEN (%s != %s)", (unpack "H*", $k1), (unpack "H*", $k2);
629 $blk->{done} = 0;
630 $seg->{done}--;
631 $self->save;
632 } else {
633 $verify .= "V";
634 }
635 }
636 }
637 $self->log ("verified segment $seg->{id}");
638 $self->log ($verify);
639
640 if ($seg->{done} >= $seg->{todo} && !$seg->{finished}) {
641 $self->log ("verified segment OK $seg->{id}");
642 $seg->{finished}++;
643 $segments--;
644 } else {
645 $self->log ("verified segment NOT OK $seg->{id}");
646 }
647 }
648 }
649
650 last unless $segments;
651
652 $sig->wait;
653 }
654
655 $self->log ("decoding < $self->{job} $self->{file} $filesize >");
656
657 for my $seg (@segments) {
658 my @part;
659 my @idx;
660 my @blk = map $blk->[$_], sort { $a <=> $b } @{$seg->{blk}};
661
662 for my $id (0 .. $#blk) {
663 my $blk = $blk[$id];
664 next unless $blk->{done};
665
666 push @part, [$self->{fh}, $blk->{offset}];
667 push @idx, $id;
668
669 last if @idx == $seg->{todo};
670 }
671
672 my $fec = new Algorithm::FEC
673 $seg->{todo},
674 scalar @blk,
675 $seg->{blksize};
676
677 $fec->shuffle (\@part, \@idx);
678
679 # now copy check blocks to their destination position
680 for my $i (0 .. $#idx) {
681 next if $idx[$i] == $i;
682
683 my $src = $part[$i];
684 $part[$i] = [$self->{fh}, $blk[$i]{offset}];
685 $fec->copy ($src, $part[$i]);
686 }
687
688 $fec->set_decode_blocks (\@part, \@idx);
689 $fec->decode;
690 }
691
692 my $sha1 = new Digest::SHA1;
693 open my $dd, "-|", "head -c$filesize \Q$self->{file}\E"
694 or die "DD: $!";
695 #$dd = Coro::Handle::unblock $dd;
696 $sha1->addfile ($dd);
697 $sha1 = $sha1->hexdigest;
698
699 if (exists $p->{meta}{document}[0]{info}{checksum}
700 and $p->{meta}{document}[0]{info}{checksum} ne $sha1) {
701 $self->log ("META: $p->{meta}{document}[0]{info}{checksum} and real checksum $sha1 for $filesize DIFFER");
702 # $self->feedback ("CHECKSUM ERROR");
703 # terminate;
704 }
705
706 truncate $self->{fh}, $filesize;
707 sysseek $self->{fh}, 0, 0;
708
709 return ["finish", 1];
710 } else {
711 $self->log ("splitfile algo '$splitfile->{algo_name}' unknown");
712 $self->feedback ("algo unknown");
713 terminate;
714 }
715 }
716
717 package main;
718
719 $|=1;
720
721 async {
722 for (<\Q$QUEUE_HOME\E/*.j>) {
723 job->new_from_file ($_);
724 print "J";
725 }
726 print "\n";
727 };
728
729 open my $stdin , "<&0" or die;
730 open my $stdout, ">&1" or die;
731 cmdline unblock $stdin, unblock $stdout;
732
733 &Coro::Event::loop;
734