home *** CD-ROM | disk | FTP | other *** search
/ InfoMagic Source Code 1993 July / THE_SOURCE_CODE_CD_ROM.iso / languages / perl / scripts-convex / clip < prev    next >
Encoding:
Text File  |  1992-07-15  |  15.7 KB  |  615 lines

  1. #!/usr/local/bin/perl
  2. 'di';
  3. 'ig00';
  4. #
  5. # $Header: /tmp_mnt/home/netlabs1/lwall/pl/RCS/clip,v 1.1 92/07/13 12:37:09 lwall Exp Locker: lwall $
  6. #
  7. # $Log:    clip,v $
  8. # Revision 1.1  92/07/13  12:37:09  lwall
  9. # Initial revision
  10.  
  11. $HOME = $ENV{HOME}
  12.      || $ENV{LOGDIR}
  13.      || (getpwuid($<))[7]
  14.      || die "No home directory!!!\n";
  15.  
  16. # Configurable parameters, may be overridden in .cliprc
  17.  
  18. $SPOOLDIR = "/usr/spool/news";        # Where news articles are stored.
  19. $NEWSLIB = "/usr/lib/news";        # Where Cnews keeps history file.
  20. $HOMETMP = "$HOME/tmp";            # Where clip should put output.
  21. $HOMEBIN = "$HOME/bin";            # Where clip should install N script.
  22. $MAXLOAD = 3;                # What load average to suspend at.
  23. $NICE = 16;                # What priority to run at.
  24. $DEBUG = 0;                # Whether to be noisy.
  25.  
  26. $CLIPRC = "$HOME/.cliprc";
  27. require $CLIPRC;
  28.  
  29. # Everything from here on should be machine independent.
  30.  
  31. open(N0, "$HOMETMP/n0");
  32. $oldpid = <N0> + 0;
  33. close N0;
  34. if ($oldpid) {
  35.     die "Already a clip process running ($oldpid)\n" if kill 0, $oldpid;
  36. }
  37.  
  38. die "You must call both &NGSKIP and &SCANNER in $CLIPRC\n"
  39.     unless defined &ngskip && defined &scanner;
  40.  
  41. sub fixmsg {
  42.     local($_, $file, $line) = @_;
  43.     $line -= ($Preamble =~ y/\n//);
  44.     s/ file \(eval\) at line (\d+)/" $file at line " .  ($1 + $line)/eg;
  45.     s/ at \(eval\) line (\d+)/" in $file at line " .  ($1 + $line)/eg;
  46.     # $* = 1;s/^/$0: /g; # too noisy to have this
  47.     die;
  48. }
  49.  
  50. sub NGSKIP {
  51.     local($userstuff) = @_;
  52.     if ($userstuff =~ tr/\n// >= 3 && $userstuff !~ /study/) {
  53.     $study = "\t\tstudy;\n";
  54.     }
  55.     else {
  56.     $study = "";
  57.     }
  58.     $eval = (($Preamble = <<'END1' . $study) . $userstuff . <<'END2');
  59.         sub ngskip {
  60.         local($_) = $nglist;
  61.         eval {
  62.             &skip if /^cancelled$/;
  63. END1
  64.         };
  65.         if ($@) {
  66.             $@ = "" if $@ eq "You should never see this\n";
  67.             die $@ if $@;
  68.         }
  69.         }
  70. END2
  71.     print STDERR $eval if $DEBUG & 1;
  72.     eval $eval;
  73.     &fixmsg($@, (caller)[1,2]) if $@;
  74.     1;
  75. }
  76.  
  77. sub SCANNER {
  78.     local($userstuff) = @_;
  79.     $eval = (($Preamble=<<'END1') . $userstuff . <<'END2');
  80.         sub scanner {
  81.         while (<ART>) {
  82.             $totalhits = 0;
  83.             do {
  84.             study;
  85.             $hits = 0;
  86. END1
  87.             $totalhits += $hits;
  88.             } while $hits;
  89.             &printhit if $totalhits;
  90.         }
  91.         }
  92. END2
  93.     print STDERR $eval if $DEBUG & 1;
  94.     eval $eval;
  95.     &fixmsg($@, (caller)[1,2]) if $@;
  96.     1;
  97. }
  98.  
  99. require "timelocal.pl";
  100.  
  101. fork && exit;            # avoid nohup behavior
  102.  
  103. $pid = $$;
  104.  
  105. $pmeter = fork;
  106. defined $pmeter || die "can't fork: $!";
  107. if ($pmeter == 0) {
  108.     &pmeter($MAXLOAD, $pid, $DEBUG & 2);
  109.     die "Not reached";
  110. }
  111.  
  112. if ($HOMEBIN) {
  113.     system "echo kill -HUP $pid >$HOMEBIN/N";
  114.     system "(echo ps $pid; echo ps $pmeter) >$HOMEBIN/P";
  115.     chmod 0755, "$HOMEBIN/N";
  116.     chmod 0755, "$HOMEBIN/P";
  117. }
  118.  
  119. setpriority(0, 0, $NICE);        # set very slow priority
  120.  
  121. $date = shift;
  122. chop($date = `cat $HOME/.lastclip`) if !$date && -f "$HOME/.lastclip";
  123. $date = &lidate($date);
  124.  
  125. chdir $SPOOLDIR || die "Can't cd: $!\n";
  126.  
  127. select(STDERR); $| = 1;
  128. select(STDOUT); $| = 1;
  129.  
  130. &CATCH;
  131. $SIG{HUP} = CATCH;        # send SIGHUP to switch to new history file
  132. $SIG{USR1} = RESTART;        # send SIGUSR1 to re-execute clip
  133. $SIG{ALRM} = IGNORE;        # so we can send ourselves an alarm safely
  134.  
  135. $r = "\r" if -t STDOUT;
  136.  
  137. ($dev,$ino,$mode,$nlink,$uid) = stat STDOUT;
  138. $origuid = $uid;
  139. for (;;) {
  140.   LOGLINE:
  141.     while (<LOG>) {
  142.     $pos = tell(LOG);
  143.     chop;
  144.     ($messid,$date,$nglist) = split(/\t/);
  145.     $0 = "clip at $messid" if $DEBUG & 1;
  146.     $wanted = 1;
  147.     &ngskip;
  148.     next LOGLINE unless $wanted;
  149.     $date = &gidate($date);
  150.     ($ng,$art) = split(m![ /]!,$nglist);
  151.     next unless $art;
  152.     $ng =~ y!.!/!;
  153.     open(ART,"$ng/$art") || next;
  154.     next if -s ART > 100_000;
  155.     $count = 0;
  156.     ++$slept;        # to force quick update after big batch
  157.     $/ = '';
  158.     $header = <ART>;
  159.     $_ = '';
  160.     &scanner;
  161.     close ART;
  162.     $sleep = 5;
  163.     }
  164.     continue {
  165.     $/ = "\n";
  166.     }
  167.     print STDERR "clip: caught up$r\n" unless $tailing++;
  168.     sleep $sleep;
  169.     $slept += $sleep;
  170.     if ($slept > 300) {
  171.     $slept = 0;
  172.     if ($date != $lastdate) {
  173.         ($dev,$ino,$mode,$nlink,$uid,$gid) = stat STDOUT;
  174.         exit unless $uid == $origuid;
  175.         open(LASTDATE,">$HOME/.lastclip");
  176.         print LASTDATE &cdate($date),"\n";
  177.         close LASTDATE;
  178.         $lastdate = $date;
  179.     }
  180.     ($dev,$ino) = stat("$NEWSLIB/history");
  181.     if ($dev != $logdev || $ino != $logino) {
  182.         print STDERR "************* NEW HISTORY FILE *************$r\n";
  183.         open(LOG,"$NEWSLIB/history") || die "Can't open log: $!\n";
  184.         ($logdev, $logino) = stat LOG;
  185.         &seekdate($date);
  186.         $tailing = 0;
  187.         $slept = 300;        # force immediate update of lastclip.
  188.     }
  189.     }
  190.     $sleep++ if $sleep < 120;
  191.     seek(LOG,$pos,0);
  192. }
  193.  
  194. sub hit {
  195.     ($before,$match,$after) = ($`,$&,$');
  196.     $match =~ s/(.)/_\b$1/g;
  197.     $_ = "$before$match$after";
  198.     study;
  199.     $hits++;
  200.     unless ($count++) {
  201.     $header =~ /From: (.*)/;
  202.     ($from = $1) =~ s/.*\((.*)\).*/$1/;
  203.     print("$r\n\@$ng/$art  \tFrom: $from$r\n") || exit;
  204.     $header =~ /Subject: (.*)/ && print "Subject: $1$r\n";
  205.     $0 = "clip " . &cdate($date);
  206.     }
  207. }
  208.  
  209. sub skip {
  210.     $wanted = 0;
  211.     die "You should never see this\n";
  212. }
  213.  
  214. sub printhit {
  215.     if (length() > 500) {
  216.     $wanted = '';
  217.     for ($line = 0; /.*\n/g; $line++) {
  218.         if ($& =~ /_\010/) {
  219.         for ($w = $line - 3; $w <= $line + 3; $w++) {
  220.             vec($wanted,$w,1) = 1;
  221.         }
  222.         }
  223.     }
  224.     $unlines = 0;
  225.     for ($line = 0; /.*\n/g; $line++) {
  226.         if (vec($wanted,$line,1)) {
  227.         print($&,$r) || exit;
  228.         $unlines = 0;
  229.         }
  230.         else {
  231.         print("...\n$r") || exit unless $unlines++;
  232.         }
  233.     }
  234.     }
  235.     else {
  236.     s/\n/\n$r/g if $r;
  237.     print() || exit;
  238.     }
  239. }
  240.  
  241. sub seekdate {
  242.     local($start) = shift;
  243.     if ($start == 9_999_999_999) {
  244.     seek(LOG,0,2);
  245.     $pos = tell(LOG);
  246.     print STDERR "$pid starting at eof...$r\n";
  247.     return;
  248.     }
  249.     ($st_dev,$st_ino,$st_mode,$st_nlink,$st_uid,$st_gid,$st_rdev,$st_size,
  250.     $st_atime,$st_mtime,$st_ctime,$st_blksize,$st_blocks) = stat(LOG);
  251.     for ($offset = $st_size - 100_000; $offset > 0; $offset -= 100_000) {
  252.     if (seek(LOG,$offset,0)) {
  253.         $_ = <LOG>;            # probably starts in middle of a line
  254.         $_ = <LOG>;
  255.         ($messid,$date,$nglist) = split(/\t/);
  256.         $date = &gidate($date);
  257.         last if $date < $start;
  258.     }
  259.     else {
  260.         $offset = -1;
  261.     }
  262.     }
  263.     seek(LOG,0,0) if $offset < 0;
  264.     while (<LOG>) {
  265.     ($messid,$date,$nglist) = split(/\t/);
  266.     $date = &gidate($date);
  267.     last if $date >= $start;
  268.     }
  269.     $pos = tell(LOG);
  270.     $pct = int($pos * 100 / $st_size);
  271.     print STDERR "$pid starting at $pct% for $start...$r\n";
  272.     $0 = "clip start at $messid" if $DEBUG & 1;
  273. }
  274.  
  275. sub CATCH {
  276.     &openout;
  277.     open(LOG,"$NEWSLIB/history") || die "Can't open log: $!\n";
  278.     ($logdev, $logino) = stat LOG;
  279.     $date = 9_999_999_999 unless $date;
  280.     &seekdate($date);
  281.     $tailing = 0;
  282.     $slept = 300;        # force immediate update of lastclip.
  283.     $SIG{HUP} = CATCH;
  284.     kill $$, ALRM;
  285. }
  286.  
  287. sub RESTART {
  288.     kill 9, $pmeter if $pmeter;
  289.     exec "clip";
  290.     die "Couldn't exec clip: $!\n";
  291. }
  292.  
  293. sub cdate {
  294.     ($sec,$min,$hr,$mday,$mon,$year) = localtime($date);
  295.     sprintf("%02d/%02d/%02d %02d:%02d",$mon+1,$mday,$year,$hr,$min);
  296. }
  297.  
  298. sub lidate {
  299.     $_[0] =~ m#(\d+)/(\d+)/(\d+) (\d+):(\d+)#
  300.       ? &timelocal(0, $5, $4, $2, $1-1, $3)
  301.       : $_[0];
  302. }
  303.  
  304. sub gidate {
  305.     $_[0] =~ m#(\d+)/(\d+)/(\d+) (\d+):(\d+)#
  306.       ? &timegm(0, $5, $4, $2, $1-1, $3)
  307.       : $_[0];
  308. }
  309.  
  310. sub openout {
  311.     rename("$HOMETMP/n8", "$HOMETMP/n9");
  312.     rename("$HOMETMP/n7", "$HOMETMP/n8");
  313.     rename("$HOMETMP/n6", "$HOMETMP/n7");
  314.     rename("$HOMETMP/n5", "$HOMETMP/n6");
  315.     rename("$HOMETMP/n4", "$HOMETMP/n5");
  316.     rename("$HOMETMP/n3", "$HOMETMP/n4");
  317.     rename("$HOMETMP/n2", "$HOMETMP/n3");
  318.     rename("$HOMETMP/n1", "$HOMETMP/n2");
  319.     rename("$HOMETMP/n0", "$HOMETMP/n1");
  320.     open(STDOUT, ">$HOMETMP/n0");
  321.     open(STDERR, ">&STDOUT");
  322. }
  323.  
  324. sub pmeter {
  325.     # ($loadavg, $pid, $debug) = @ARGV;
  326.     local($loadavg, $pid, $debug) = @_;
  327.     $running = 1;
  328.     $0 = "pmeter @_";
  329.  
  330.     while (kill 0, $pid) {
  331.     `/usr/ucb/uptime` =~ /load average:\s+([\d.]+)/
  332.         || die "Can't run uptime: $!\n";;
  333.  
  334.     if ($1 > $loadavg) {
  335.         kill 'STOP', $pid;
  336.         if ($running) {
  337.         print STDERR "stopping at $1\r\n" if $debug;
  338.         $0 = "pmeter (stopped $pid at $loadavg)";
  339.         $running = 0;
  340.         }
  341.     }
  342.     else {
  343.         kill 'CONT', $pid;
  344.         if (!$running) {
  345.         print STDERR "starting at $1\r\n" if $debug;
  346.         $0 = "pmeter (started $pid at $loadavg)";
  347.         $running = 1;
  348.         }
  349.     }
  350.     sleep 120;
  351.     }
  352. }
  353. ###############################################################
  354.  
  355.     # These next few lines are legal in both Perl and nroff.
  356.  
  357. .00;                       # finish .ig
  358.  
  359. 'di           \" finish diversion--previous line must be blank
  360. .nr nl 0-1    \" fake up transition to first page again
  361. .nr % 0         \" start at page 1
  362. '; __END__ ##### From here on it's a standard manual page #####
  363.  
  364. .TH CLIP 1 "July 14, 1992"
  365. .de M           \" man page reference
  366. \\fI\\$1\\fR\\|(\\$2\)\\$3
  367. ..
  368. .AT 3
  369. .SH NAME
  370. clip \- personal news clipping service
  371. .br
  372. pmeter \- start and stop a process according to load average
  373. .SH SYNOPSIS
  374. .B clip
  375. [
  376. .I date
  377. ]
  378. .SH DESCRIPTION
  379. .I Newsclip
  380. scans incoming news by following the C news history file as it grows
  381. and examining each article listed there for patterns of interest.
  382. .PP
  383. If a date argument is supplied on the command line (in the form 
  384. .IR "``mm/dd/yy hh:mm''" , 
  385. including the embedded space)
  386. .I clip
  387. will read articles since that date.  With no arguments, it examines
  388. the user's ~/.lastclip file to know when it left off scanning;
  389. if no such file exists, every news article will be scanned.
  390. .PP
  391. Patterns and options are specified in a .cliprc file situated
  392. in your home directory.  The .cliprc file is simply a gob of Perl
  393. code that will be evaluated by clip after it has set its default
  394. options but before it actually goes out to do anything.  The only
  395. mandatory items are a call to each of two subroutines: &NGSKIP and
  396. &SCANNER.  A typical .cliprc file looks like this:
  397. .nf
  398.  
  399. .ne 6
  400.     # Options I want to override.
  401.     $MAXLOAD = 4;
  402.  
  403.     # Newsgroups I always read anyway, so don't bother.
  404.     &NGSKIP( <<'END' );
  405.         &skip if /comp\e.lang\e.perl/;
  406.         &skip if /rec\e.humor\e.funny/;
  407.     END
  408.  
  409. .ne 8
  410.     # Patterns I'm interested in scanning for.
  411.     &SCANNER( <<'END' );
  412.         &hit if /betty[^\e0]boop/i;
  413.         &hit if /roger[^\e0]rabbit/i;
  414.         &hit if /\ebw.*coyote\eb/i;
  415.  
  416.         if (/\ebacme\eb/i) {{
  417.             next if $` =~ /roadrunner\e@$/;
  418.             &hit;
  419.         }}
  420.  
  421.         if (/\eblooney\eb/i) {{
  422.             next if $' =~ /^\es*bin/;
  423.             next if $` =~ /!$/;
  424.             next if $` =~ /fudd\e@/i;
  425.             next if $nglist =~ /alt\e.crazy\e.people/;
  426.             &hit;
  427.         }}
  428.     END
  429.  
  430. .fi
  431. The argument to the &NGSKIP routine is a sequence of zero or more Perl
  432. statements that call &skip if the current article is crossposted to a
  433. newsgroup that we don't want to scan (generally because we'll read the
  434. newsgroup anyway).  Before your code is called, the $_ variable is
  435. automatically set to the list of newsgroups from the history file.
  436. You can invert the logic and just pick the newsgroups you want by
  437. saying:
  438. .nf
  439.  
  440.  
  441. .ne 9
  442.     &NGSKIP(<<'END');
  443.         study;
  444.         {
  445.             last if /comp\e.lang\e.perl/;
  446.             last if /comp\e.org\e.usenix/;
  447.             last if /comp\e.unix\e.bsd/;
  448.             &skip;
  449.         }
  450.     END
  451.  
  452. .fi
  453. .PP
  454. The argument to the &SCANNER routine is a sequence of one or more Perl
  455. statements that call &hit if an interesting pattern has been spotted.
  456. Before your code is called, the $_ variable is set to the current paragraph
  457. of the current article (articles are read and scanned paragraph by paragraph).
  458. Newsclip will snip excerpts from any paragraph containing patterns,
  459. underlining the patterns it found.  Note that some patterns are
  460. unqualified hits, while others are hits only if some other pattern doesn't
  461. match.  (It's important to the underlining algorithm that the value
  462. of $& continue to contain the value of the last 
  463. .I successful
  464. match \(em all
  465. of the exceptions are expected to fail.)  For the purposes of exception
  466. scanning, the variables $` and $' are automatically set to the preceding
  467. and subsequent text by the successful initial pattern match.  In addition,
  468. .I clip 
  469. sets the variable $nglist to the current list of newsgroups, and
  470. the variable $header to be the header of the current article.
  471. .PP
  472. The use of double curlies is merely to allow the ``next'' command to fall
  473. through to the next test, since ``next'' will simply fall out of an ordinary
  474. block.  The ``last'' command could also have been used, but would be less
  475. intuitive.  Other special thingies you might want to use include \eb
  476. to assert a word boundary, [^\e0] to match any character but a null,
  477. including a newline, and any other regular expression goodies you can
  478. think up to detect spelling errors and variants.  Tom Christiansen
  479. searches for his name with:
  480. .nf
  481.  
  482. .ne 4
  483.     if (/\ebtchrist\eb/i || /tom[^\e0]christ(ia|e)ns[eo]n/i) {{ # grr...
  484.         next if /\en\es*Tom Christiansen\es+tchrist\e@convex.com/;
  485.         &hit;
  486.     }}
  487.  
  488. .fi
  489. .PP
  490. .I Newsclip
  491. forks another copy of itself,
  492. called
  493. .IR pmeter ,
  494. which runs 
  495. .M uptime 1
  496. every two minutes to make sure the load hasn't gone too
  497. high.  If it has, 
  498. .I pmeter
  499. suspends 
  500. .I clip 
  501. with a SIGSTOP,
  502. waiting until the load average goes back down before
  503. allowing 
  504. .I clip 
  505. to continue.
  506. Both 
  507. .I pmeter
  508. and 
  509. .I clip
  510. continually
  511. muck with their own externally-visible argument list
  512. to keep folks running
  513. .M ps 1
  514. amused.  This has no effect if your operating system doesn't
  515. support changes to argv being noticed by ps.
  516. .PP
  517. Output by default goes to ~/tmp/n0.  When 
  518. .I clip 
  519. is hit by a SIGHUP,
  520. it renames n8 to n9, n7 to n8, ..., n0 to n1, and creates a new n0
  521. file.  For handiness, when 
  522. .I clip 
  523. starts up, it creates a tiny script
  524. called ``N'' that will send the SIGHUP to the correct process.  Typically
  525. one runs N in the morning to set up a new n0 file, and then reads ~/tmp/n1
  526. to see what the scanner found since the last time you ran N.  A companion
  527. script, named ``P'', runs 
  528. .I ps
  529. on the 
  530. .I clip
  531. and 
  532. .I pmeter
  533. processes.  (This assumes a BSD-style 
  534. .I ps 
  535. program.)
  536. .PP
  537. The first line of each article reference begins with an @, in case you
  538. want to cut and paste that line to something that looks up the article
  539. for you.  I use the following rn macro.  Unfortunately I have to paste
  540. the line twice because of how rn eats typeahead, but hey, it beats a
  541. kick in the head.  This macro should all be on one line \(em it's broken in
  542. two so that nroff doesn't get upset:
  543. .nf
  544.  
  545. .ne 12
  546. @ %(%m=n?:%(%m!=a?q:)q)%(%"^Jng/art: "=\e([^ @]*\e)/\e([^ ]*\e)?
  547. g%`perl -e '($_="%1")=~tr#/#.#;print'`^J.%2^J:%(%m=n?^L:))
  548.  
  549. .fi
  550. .PP
  551. Interesting variables to set in your .cliprc file include the following:
  552. .TP 15
  553. .B $SPOOLDIR
  554. Where news articles are stored; defaults to /usr/spool/news.
  555. .TP 
  556. .B $NEWSLIB 
  557. Where Cnews keeps its history file; defaults to /usr/lib/news.
  558. .TP 
  559. .B $HOMETMP 
  560. Where clip should put its output files;
  561. defaults to ~/tmp.
  562. .TP 
  563. .B $HOMEBIN 
  564. Where clip should install N script;
  565. defaults to ~/bin.
  566. .TP 
  567. .B 
  568. $MAXLOAD 
  569. What load average to suspend at; defaults to 3.
  570. .TP 
  571. .B $NICE 
  572. What priority to run at; defaults to 16.
  573. .TP 
  574. .B $DEBUG
  575. Whether to be noisy; defaults to 0.  A value
  576. with the 1 bit set causes 
  577. .I clip
  578. to be noisy;
  579. a value with the 2 bit set causes
  580. .I pmeter
  581. to be noisy.
  582. .SH ENVIRONMENT
  583. HOME or LOGDIR    
  584. .SH SIGNALS
  585. .nf 
  586. SIGHUP    Cycle log files.
  587. SIGUSR1    Re-exec oneself in case .cliprc changes.
  588. SIGALRM    Make clip wake up early.
  589. .fi
  590. .SH FILES
  591. .nf
  592. .ta \w'$HOME/.cliprc   'u
  593. $HOME/.cliprc    Your clipping preferences.
  594. $HOME/.lastclip    Date and time of last article scanned.
  595. $HOMETMP/n[0-9]    Log files.
  596. $HOMEBIN/N    Cycle log files: n8 -> n9, n7 -> n8, etc.
  597. .fi 
  598. .SH AUTHOR
  599. Larry Wall, with heckling by Tom Christiansen.
  600. .SH "SEE ALSO"
  601. .M perl 1 ,
  602. .M rn 1 ,
  603. .M ps 1 ,
  604. .M uptime 1 ,
  605. .M newsmaint 8 .
  606. .SH DIAGNOSTICS
  607. Obscure diagnostics are an obsolete concept rooted in the notion that
  608. programs must be shoehorned into memory.
  609. .SH BUGS
  610. There ought to be a way for multiple users to share a single clip process.
  611. .PP
  612. It doesn't work on NNTP-only systems.
  613. .ex
  614.