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