ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-FCP/bin/fmd
Revision: 1.20
Committed: Fri Dec 21 09:08:18 2007 UTC (18 years, 8 months ago) by root
Branch: MAIN
Changes since 1.19: +9 -7 lines
Log Message:
*** empty log message ***

File Contents

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