ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-FCP/bin/fmd
Revision: 1.14
Committed: Tue Nov 28 15:18:17 2006 UTC (19 years, 9 months ago) by root
Branch: MAIN
Changes since 1.13: +11 -7 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;
98 use Coro::Event;
99 use Coro::Handle;
100 use Coro::Signal;
101 use Coro::Timer;
102 use Coro::AIO;
103 use List::Util;
104 use Digest::SHA1;
105 use Algorithm::FEC;
106 use Digest::SHA1;
107 use Net::FCP::Util;
108 use POSIX ();
109
110 Coro::AIO::max_poll_time 0.01;
111 Coro::AIO::min_parallel 1;
112 Coro::AIO::max_parallel 2;
113
114 $|=1;
115
116 our $MAX_TXN = 250; # use max. this many transactions in parallel
117 our @HTL = (5,12,20);
118 our $VERIFY_CHK = 1; # verify all blocks, again and again (only useful to debugging).
119 our $FMD_HOME = $ENV{FMD_HOME} || "$ENV{HOME}/fmd";
120
121 our $FCP = new Net::FCP;
122
123 defined $FMD_HOME
124 or die "you currently must define FMD_HOME to a persistent directory";
125
126 mkdir $FMD_HOME, 0700;
127
128 our $QUEUE_HOME = "$FMD_HOME/tmp";
129 mkdir $QUEUE_HOME, 0700;
130 our $DONE_HOME = "$FMD_HOME/done";
131 mkdir $DONE_HOME, 0700;
132
133 our %job;
134
135 sub push_key {
136 my ($key, $title) = @_;
137
138 for my $job (values %job) {
139 if ($job->{p}{key} eq $key) {
140 warn "job $job->{id} already works on this, not adding";
141 return;
142 }
143 }
144
145 my $job = job->new_from_key ($key, $title);
146 warn "added as job $job->{id}\n";
147 $job;
148 }
149
150 sub cmdline {
151 my ($i, $o) = @_;
152
153 my $cmd = async {
154 my $job;
155 while (print $o "> " and defined ($_ = <$i>)) {
156 chomp;
157 if (/<attach(?:ment|ed|)>(.*) \* (CHK[^<]+)<\/attach/) {
158 $job = push_key $2, $1;
159 } elsif (/(CHK\@[a-zA-Z0-9,~\-]{54})[\/ ]+(.*?)\s*$/) {
160 $job = push_key $1, $2;
161 } elsif (s/^(\d+)//) {
162 if ($job = $job{$1}) {
163 print $o delete $job->{log};
164 if ($job->{input}) {
165 }
166 }
167 redo;
168 } elsif (/^q/) {
169 close $i;
170 close $o;
171 } elsif (/^l/) {
172 for my $job (sort { $a->{id} <=> $b->{id} } values %job) {
173 print $o "$_ $job->{id}: $job->{p}{key} $job->{p}{title} $job->{status}\n";
174 }
175 } elsif (/^pri\s*(\d+)/) {
176 if ($job) {
177 $job->{p}{pri} = $1;
178 $job->save;
179 }
180 } elsif (/^s$/) {
181 print $o $job->show if $job;
182 } elsif (/^k$/) {
183 $job->kill if $job;
184 } elsif (/^p(.*)/) {
185 print +(join " ", eval $1), "\n";
186 } elsif (/\S/) {
187 #
188 } else {
189 print $o "?\n";
190 }
191 for my $job (sort { $a->{id} <=> $b->{id} } values %job) {
192 if ($job->{_input}) {
193 print $o "> $job->{id} $job->{title} $job->{status}\n";
194 }
195 }
196 }
197 };
198 }
199
200 package job;
201
202 use strict;
203
204 use Coro;
205 use Coro::AIO;
206 use Fcntl;
207 use IO::Handle;
208 use Array::Heap2;
209
210 our $count = 0;
211
212 sub new {
213 my $class = shift;
214
215 my $self = bless { @_ }, $class;
216
217 $self->{p}{title} !~ /\//
218 or die "$self->{p}{title}: malformed key\n";
219
220 $self->{p}{title} =~ s/\s+$//;
221 $self->{p}{title} =~ s/\n/_/g;
222
223 $self->{id} = ++$count;
224 $self->{job} ||= "$QUEUE_HOME/" . Time::HiRes::time . ":$count.j";
225
226 $job{$self->{id}} = $self;
227
228 $self->save;
229 $self->start;
230 $self;
231 }
232
233 sub new_from_key {
234 my ($class, $key, $title) = @_;
235 $class->new (p => { key => $key, title => $title, state => "examine" });
236 }
237
238 sub new_from_file {
239 my ($class, $path) = @_;
240 $class->new (job => $path, p => Storable::retrieve $path);
241 }
242
243 sub save {
244 my ($self) = @_;
245 Storable::nstore $self->{p}, "$self->{job}~";
246 rename "$self->{job}~", $self->{job};
247 }
248
249 sub clean {
250 my ($self) = @_;
251
252 delete $job{$self->{id}};
253 $self->save;
254 rename $self->{job}, "$DONE_HOME/$self->{p}{title}.job";
255 unlink $self->{job};
256 }
257
258 sub kill {
259 my ($self) = @_;
260
261 $self->clean;
262 $self->{coro}->cancel;
263 }
264
265 our @queue;
266 our $queue_change = new Coro::Signal;
267 our $queue_alloc = 0;
268
269 async {
270 for (;;) {
271 while (@queue
272 and (($queue[0][0] > 10 and $queue_alloc < $MAX_TXN)
273 or ($queue[0][0] > 1 and $queue_alloc < $MAX_TXN - 3)
274 or $queue_alloc < $MAX_TXN - 5)) {
275 (pop_heap @queue)->[1]->send;
276 $queue_alloc++;
277 Coro::Timer::sleep 0.05;
278 }
279 $queue_change->wait;
280 }
281 };
282
283 sub txn_begin {
284 my ($pri) = @_;
285 my $sig = new Coro::Signal;
286
287 #warn "txn_begin $pri\n";#d#
288 push_heap @queue, [$pri, $sig];
289 $queue_change->send;
290 $sig->wait;
291 }
292
293 sub txn_end {
294 $queue_alloc--;
295 $queue_change->send;
296 }
297
298 sub fetch_uri {
299 my ($pri, $uri) = @_;
300
301 for(my $count = 1; ; $count += 0.3) {
302 for my $htl (@HTL) {
303 txn_begin time + $htl + $count;
304 my $sig = new Coro::Signal;
305 my ($meta, $data) = eval { @{ $FCP->client_get ($uri, $htl) } };
306 txn_end;
307 if ($@) {
308 if (UNIVERSAL::isa ($@, Net::FCP::Exception::)) {
309 if ($@->type ("data_not_found")
310 || $@->type ("route_not_found")) {
311 next;
312 }
313 if ($@->type ("short_data")) {
314 warn "(short_data, redo)\n";
315 redo;
316 }
317 die;
318 }
319 }
320 if (defined $data) {
321 return ($meta, $data);
322 }
323 }
324 }
325
326 die;
327 }
328
329 sub log {
330 my ($self, $text) = @_;
331 my $time = POSIX::strftime "%H:%M:%S", localtime time;
332 warn "$time $self->{id},$self->{p}{pri}: $text\n";
333 $self->{log} .= "$time $text\n";
334 }
335
336 sub feedback {
337 my ($self, $prompt) = @_;
338 $self->{input} = [$Coro::current, $prompt];
339 Coro::schedule;
340 }
341
342 sub show {
343 my ($self) = @_;
344
345 "ID: $self->{id}\n"
346 . "Title: $self->{p}{title}\n"
347 . "Blocks#: " . @{$self->{p}{blk}} . "\n"
348 . "Blocks: " . (join "", map {
349 $_->{done} ? "+" : "-"
350 } @{$self->{p}{blk}}) . "\n" .
351 ""
352 }
353
354 our $id;
355
356 sub MAXSEG (){ 128*1024*1024 }
357 sub MINSEG (){ 6* 128*1024 }
358
359 sub blocksize($) {
360 return
361 $_[0] >= 64*1024*1024 ? 1024*1024
362 : $_[0] >= 32*1024*1024 ? 512*1024
363 : $_[0] >= 1024*1024 ? 256*1024
364 : 128*1024;
365 }
366
367 sub start {
368 my ($self) = @_;
369
370 $self->{p}{pri} ||= 1;
371 $self->{p}{state} ||= "examine";
372
373 $self->{job} =~ /^(.*)\.j$/ or die "$self->{job}: missing .j";
374 $self->{file} = "$1.d";
375 $self->{fh} = aio_open $self->{file}, O_RDWR|O_CREAT, 0600
376 or die "$self->{file}: $!";
377
378 $self->{status} = "starting";
379 $self->{coro} = async {
380 for(;;) {
381 my ($state, @args) = ref $self->{p}{state} ? @{$self->{p}{state}} : $self->{p}{state};
382 my $next = eval { $self->can ("state_$state")->($self, @args) };
383 if ($@) {
384 $self->log ($@);
385 $next = $self->feedback ("continue with state: ");
386 }
387 $self->log ($self->{status} = "STATE CHANGE: ". join ", ", ref $next ? @$next : $next);
388 $self->{p}{state} = $next;
389 $self->save;
390 }
391 };
392 }
393
394 sub state_finish {
395 my ($self, $save) = @_;
396
397 if ($save) {
398 aio_fsync $self->{fh};
399 close $self->{fh};
400
401 aio_unlink "$DONE_HOME/$self->{p}{title}";
402 aio_link $self->{file}, "$DONE_HOME/$self->{p}{title}"
403 and die "link: $self->{file} => $DONE_HOME/$self->{p}{title}: $!";
404 system "sync";
405 }
406 $self->clean;
407
408 aio_unlink $self->{file};
409
410 $self->{status} = "finished";
411 $self->feedback ("finished");
412 terminate;
413 }
414
415 sub state_examine {
416 my ($self) = @_;
417 my $p = $self->{p};
418
419 $self->{status} = "initial fetch";
420
421 for (;;) {
422 $self->log ("fetching $p->{key} (=$p->{title})");
423
424 ($p->{meta}, $p->{data}) = fetch_uri 100, "freenet:$p->{key}";
425 $self->save;
426 #use PApp::Util; print STDERR PApp::Util::dumpval [keys %{$meta->{document}[0]{split_file}}];
427 $self->log ("type $p->{meta}{document}[0]{info}{format}");
428
429 if (my $splitfile = $p->{meta}{document}[0]{split_file}) {
430 return "splitfile";
431 } elsif ((defined $p->{data}) and (length $p->{data})) {
432 syswrite $self->{fh}, delete $p->{data};
433 aio_fsync $self->{fh};
434 return ["finish", 1];
435 }
436
437 $self->log ("EMPTY, retrying in an hour");
438 Coro::Timer::sleep 3600;
439 }
440
441 }
442
443 sub state_splitfile {
444 my ($self) = @_;
445 my $p = $self->{p};
446
447 my $splitfile = $p->{meta}{document}[0]{split_file};
448 my $filesize = hex $splitfile->{size};
449
450 if ($splitfile->{algo_name} eq "OnionFEC_a_1_2") {
451 my $data_packets = hex $splitfile->{block_count};
452 my $check_packets = hex $splitfile->{check_block_count};
453
454 my $blk = ($p->{blk} ||= []);
455
456 unless (@$blk) {
457 for (1..$data_packets) {
458 push @$blk, {
459 uri => $splitfile->{block}{sprintf "%x", $_},
460 };
461 }
462 for (1..$check_packets) {
463 push @$blk, {
464 uri => $splitfile->{check_block}{sprintf "%x", $_},
465 };
466 }
467 }
468
469 my @segments;
470 my $segments = 0;
471
472 {
473 # that is a horrible algorithm :(, these freenet freaks are... java-disabled
474 # hardcoding lots of magic parameters is soo dumb.
475 my $size = $filesize;
476 my $offset = 0;
477 my $offset2 = ($filesize & ~(1024*1024-1)) + 1024*1024; # leave enough space after last data block
478 my $idx = 0;
479 my $idx2 = $data_packets;
480 my @redundandy = (0,1,2); # maybe OnionFAC_a_1_2 means 1/2 redundancy(?)
481
482 while ($size > 0) {
483 my $segsize = $size >= MAXSEG ? MAXSEG : $size <= MINSEG ? MINSEG : $size;
484 my $blksize = blocksize $segsize;
485 my $seg =
486 {
487 id => $segments++,
488 todo => int (($segsize + $blksize - 1) / $blksize),
489 done => 0,
490 blk => [],
491 blksize => $blksize,
492 };
493
494 push @segments, $seg;
495 $size -= $segsize;
496
497 while ($segsize > 0) {
498 push @{$seg->{blk}}, $idx;
499 for ($blk->[$idx++]) {
500 $_->{offset} = $offset;
501 #$_->{size} = $blksize > $segsize ? $segsize : $blksize; # WRONG
502 $_->{size} = $blksize;
503 $_->{seg} = $seg;
504 }
505
506 $segsize -= $blksize;
507 $offset += $blksize;
508
509 if (($redundandy[0] += $redundandy[1]) >= $redundandy[2]) {
510 $redundandy[0] -= $redundandy[2];
511
512 push @{$seg->{blk}}, $idx2;
513 for ($blk->[$idx2++]) {
514 $_->{offset} = $offset2;
515 $_->{size} = $blksize;
516 $_->{seg} = $seg;
517 }
518
519 $offset2 += $blksize;
520 }
521 }
522 }
523
524 $idx == $data_packets
525 or die "$self->{id}/$p->{tile} $self->{job}\nidx $idx != data_packets $data_packets";
526 $idx2 == $data_packets + $check_packets
527 or die "$self->{id}/$p->{tile} $self->{job}\nidx2 $idx2 != data_packets $data_packets + check_packets $check_packets";
528 }
529
530 for (@$blk) {
531 ++$_->{seg}{done} if $_->{done};
532 delete $_->{htl};
533 }
534
535 my $fail = 0;
536 my $sig = new Coro::Signal;
537
538 $self->{status} = "splitfile fetch (" . @$blk . " blocks)";
539
540 my @txn;
541
542 for (;;) {
543 for my $id (0 .. $#$blk) {
544 my $blk = $blk->[$id];
545
546 next if $txn[$id] || $blk->{done} || $blk->{seg}{todo} <= $blk->{seg}{done};
547
548 my $htl = $HTL[$blk->{htl}++ % @HTL];
549 my $pri = int time + $htl / 20 * 7200 * rand;
550
551 txn_begin $pri;
552 #warn $self->{id} . ", GET<$htl, $pri>\n";#d#
553 $txn[$id] ||= $FCP->txn_client_get ($blk->{uri}, $htl)->cb(sub {
554 undef $txn[$id];
555
556 my $seg = $blk->{seg};
557
558 my ($meta, $data) = eval { @{ $_[0]->result } };
559
560 if (defined $data) {
561 $blk->{size} == length $data
562 or die sprintf "block $id expected size %d, got %d\n", $blk->{size}, length $data;
563
564 (length $data) == (aio_write $self->{fh}, $blk->{offset}, (length $data), $data, 0)
565 or die "unable to write chunk to disk, not setting valid flag";
566 aio_fsync $self->{fh};
567
568 $blk->{done} = 1;
569 $blk->{meta} = $meta->{raw} if length $meta->{raw};#d#
570 $seg->{done}++;
571 $self->save;
572
573 $::htl_sum += $htl;
574 $::htl_cnt++;
575
576 $self->log (sprintf "got block $seg->{id}.$id %d ($seg->{done}/$seg->{todo}) at htl $htl (%f) and pri $pri",
577 length $data, $::htl_sum / $::htl_cnt);
578 } else {
579 if ($@) {
580 if ($@->type ("data_not_found")) {
581 # nop
582 } elsif ($@->type ("network_error")) {
583 $self->log ("$@, retrying in 1s");
584 CORE::sleep 1;
585 } else {
586 $self->log ("$@");
587 }
588 }
589 ++$fail;
590 }
591 $self->{status} = "splitfile fetch ($seg->{done}/$seg->{todo}, $fail failed)";
592
593 txn_end;
594 $sig->send;
595 });
596 }
597
598 for my $seg (@segments) {
599 if ($seg->{done} >= $seg->{todo} && !$seg->{finished}) {
600
601 $self->log ("segment done, cancelling segment $seg->{id}");
602 for my $id (@{$seg->{blk}}) {
603 (delete $txn[$id])->cancel if $txn[$id];
604 }
605
606 my $verify;
607 for my $id (@{$seg->{blk}}) {
608 my $blk = $blk->[$id];
609 if ($blk->{done}) {
610 aio_read $self->{fh}, $blk->{offset}, $blk->{size}, my $buf, 0;
611
612 my $k1 = Net::FCP::Util::extract_chk_hash $blk->{uri};
613 my $k2 = Net::FCP::Util::generate_chk_hash $blk->{meta}, $buf;
614
615 if ($k1 ne $k2) {
616 $verify .= "v";
617 #warn sprintf "$p->{title} block $id BROKEN (%s != %s)", (unpack "H*", $k1), (unpack "H*", $k2);
618 $blk->{done} = 0;
619 $seg->{done}--;
620 $self->save;
621 } else {
622 $verify .= "V";
623 }
624 }
625 }
626 $self->log ("verified segment $seg->{id}");
627 $self->log ($verify);
628
629 if ($seg->{done} >= $seg->{todo} && !$seg->{finished}) {
630 $self->log ("verified segment OK $seg->{id}");
631 $seg->{finished}++;
632 $segments--;
633 } else {
634 $self->log ("verified segment NOT OK $seg->{id}");
635 }
636 }
637 }
638
639 last unless $segments;
640
641 $sig->wait;
642 }
643
644 $self->log ("decoding < $self->{job} $self->{file} $filesize >");
645
646 for my $seg (@segments) {
647 my @part;
648 my @idx;
649 my @blk = map $blk->[$_], sort { $a <=> $b } @{$seg->{blk}};
650
651 for my $id (0 .. $#blk) {
652 my $blk = $blk[$id];
653 next unless $blk->{done};
654
655 push @part, [$self->{fh}, $blk->{offset}];
656 push @idx, $id;
657
658 last if @idx == $seg->{todo};
659 }
660
661 my $fec = new Algorithm::FEC
662 $seg->{todo},
663 scalar @blk,
664 $seg->{blksize};
665
666 $fec->shuffle (\@part, \@idx);
667
668 # now copy check blocks to their destination position
669 for my $i (0 .. $#idx) {
670 next if $idx[$i] == $i;
671
672 my $src = $part[$i];
673 $part[$i] = [$self->{fh}, $blk[$i]{offset}];
674 $fec->copy ($src, $part[$i]);
675 }
676
677 $fec->set_decode_blocks (\@part, \@idx);
678 $fec->decode;
679 }
680
681 my $sha1 = new Digest::SHA1;
682 open my $dd, "-|", "head -c$filesize \Q$self->{file}\E"
683 or die "DD: $!";
684 #$dd = Coro::Handle::unblock $dd;
685 $sha1->addfile ($dd);
686 $sha1 = $sha1->hexdigest;
687
688 if (exists $p->{meta}{document}[0]{info}{checksum}
689 and $p->{meta}{document}[0]{info}{checksum} ne $sha1) {
690 $self->log ("META: $p->{meta}{document}[0]{info}{checksum} and real checksum $sha1 for $filesize DIFFER");
691 # $self->feedback ("CHECKSUM ERROR");
692 # terminate;
693 }
694
695 truncate $self->{fh}, $filesize;
696 sysseek $self->{fh}, 0, 0;
697
698 return ["finish", 1];
699 } else {
700 $self->log ("splitfile algo '$splitfile->{algo_name}' unknown");
701 $self->feedback ("algo unknown");
702 terminate;
703 }
704 }
705
706 package main;
707
708 $|=1;
709
710 async {
711 for (<\Q$QUEUE_HOME\E/*.j>) {
712 job->new_from_file ($_);
713 print "J";
714 }
715 print "\n";
716 };
717
718 open my $stdin , "<&0" or die;
719 open my $stdout, ">&1" or die;
720 cmdline unblock $stdin, unblock $stdout;
721
722 &Coro::Event::loop;
723