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