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