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