ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-FCP/bin/fmd
Revision: 1.12
Committed: Thu Nov 23 23:58:50 2006 UTC (19 years, 9 months ago) by root
Branch: MAIN
Changes since 1.11: +5 -3 lines
Log Message:
*** empty log message ***

File Contents

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