Powered By Blogger

Saturday, June 9, 2007

perl-proxy: Немного юмора

В связи с работой над одним левым заказом мне пришлось заинтересоваться перлопроксями и вот, что из этого получилось:

#!/usr/bin/perl -Tw

use strict;
$ENV{PATH} = join ":", qw(/usr/ucb /bin /usr/bin);
$|++;

my $VERSION_ID = q$Id: proxy,v 1.21 1998/xx/xx xx:xx:xx merlyn Exp $;
my $VERSION = (qw$Revision: 1.21 $ )[-1];

## Copyright (c) 1996, 1998 by Randal L. Schwartz
## This program is free software; you can redistribute it
## and/or modify it under the same terms as Perl itself.

### debug management
sub prefix {
   my $now = localtime;
   join "", map { "[$now] [${$}] $_\n" } split /\n/, join "", @_;
}

$SIG{__WARN__} = sub { warn prefix @_ };
$SIG{__DIE__} = sub { die prefix @_ };

&setup_signals();

### logging flags
my $LOG_PROC = 1;               # begin/end of processes
my $LOG_TRAN = 1;               # begin/end of each transaction
my $LOG_REQ_HEAD = 0;           # detailed header of each request
my $LOG_REQ_BODY = 0;           # header and body of each request
my $LOG_RES_HEAD = 0;           # detailed header of each response
my $LOG_RES_BODY = 0;           # header and body of each response

### configuration
my $HOST = 'WWW.XXX.YYY.ZZZ';
my $PORT = 3128;                   # pick next available user-port
my $SLAVE_COUNT = 8;            # how many slaves to fork
my $MAX_PER_SLAVE = 20;         # how many transactions per slave

### main
warn("running version ", $VERSION);

&main();
exit 0;

### subs
sub main {                      # return void
   use HTTP::Daemon;
   my %kids;

   my $master = HTTP::Daemon->new(LocalPort => $PORT, LocalAddr => $HOST) or die "Cannot create master: $!";
   warn("master is ", $master->url);
   ## fork the right number of children
   for (1..$SLAVE_COUNT) {
      $kids{&fork_a_slave($master)} = "slave";
   }
   {                             # forever:
      my $pid = wait;
      my $was = delete ($kids{$pid}) || "?unknown?";
      warn("child $pid ($was) terminated status $?") if $LOG_PROC;
      if ($was eq "slave") {      # oops, lost a slave
         sleep 1;                  # don't replace it right away
         #(avoid thrash)
         $kids{&fork_a_slave($master)} = "slave";
      }
   } continue { redo };          # semicolon for cperl-mode
}

sub setup_signals {             # return void
   setpgrp;                      # I *am* the leader
   $SIG{HUP} = $SIG{INT} = $SIG{TERM} = sub {
      my $sig = shift;
      $SIG{$sig} = 'IGNORE';
      kill $sig, 0;               # death to all-comers
      die "killed by $sig";
   };
}

sub fork_a_slave {              # return int (pid)
   my $master = shift;           # HTTP::Daemon

   my $pid;
   defined ($pid = fork) or die "Cannot fork: $!";
   &child_does($master) unless $pid;
   $pid;
}

sub child_does {                # return void
   my $master = shift;           # HTTP::Daemon
   my $did = 0;                  # processed count

   warn("child started") if $LOG_PROC;
   {
      flock($master, 2);          # LOCK_EX
      warn("child has lock") if $LOG_TRAN;
      my $slave = $master->accept or die "accept: $!";
      warn("child releasing lock") if $LOG_TRAN;
      flock($master, 8);          # LOCK_UN
      my @start_times = (times, time);
      $slave->autoflush(1);
      warn("connect from ", $slave->peerhost) if $LOG_TRAN;
      &handle_one_connection($slave); # closes $slave at right time
      if ($LOG_TRAN) {
         my @finish_times = (times, time);
         for (@finish_times) {
            $_ -= shift @start_times; # crude, but effective
         }
         warn(sprintf "times: %.2f %.2f %.2f %.2f %d\n", @finish_times);
      }
   } continue { redo if ++$did < $MAX_PER_SLAVE };
   warn("child terminating") if $LOG_PROC;
   exit 0;
}

sub handle_one_connection {     # return void
   use HTTP::Request;
   my $handle = shift;           # HTTP::Daemon::ClientConn

   my $request = $handle->get_request;
   print $request;
   defined($request) or die "bad request"; # XXX

   my $response = &fetch_request($request);
   warn("response: <<<\n", $response->headers_as_string, "\n>>>")
   if $LOG_RES_HEAD and not $LOG_RES_BODY;
   warn("response: <<<\n", $response->as_string, "\n>>>")
   if $LOG_RES_BODY;
   $handle->send_response($response);
   close $handle;
}

sub fetch_request {             # return HTTP::Response
   use HTTP::Response;
   use URI::URL;
   my $request = shift;          # HTTP::Request

   ## XXX
   print "Request was: " . $request->as_string;
   ## XXXX needs policy here
   my $url = URI::URL->new($request->url);

   if ($url->scheme !~ /^(https?|gopher|ftp)$/) {
      my $res = HTTP::Response->new(403, "Forbidden");
      $res->content("bad scheme: @{[$url->scheme]}\n");
      $res;
   } elsif (not $url->rel->netloc) {
      my $res = HTTP::Response->new(403, "Forbidden");
      $res->content("relative URL not permitted\n");
      $res;
   } else {
      ## validated request, get it!
      warn("processing url is $url") if $LOG_TRAN;
      &fetch_validated_request($request);
   }
}

BEGIN {                         # local static block
   my $agent;                    # LWP::UserAgent

sub fetch_validated_request {         # return HTTP::Response
   my $request = shift;         # HTTP::Request

   $agent ||= do {
      use LWP::UserAgent;
      my $agent = LWP::UserAgent->new;
      $agent->agent("proxy/$VERSION " . $agent->agent);
      $agent->env_proxy;
      $agent;
   };

   warn("fetch: <<<\n", $request->headers_as_string, "\n>>>")
   if $LOG_REQ_HEAD and not $LOG_REQ_BODY;
   warn("fetch: <<<\n", $request->as_string, "\n>>>")
   if $LOG_REQ_BODY;

   my $response = $agent->simple_request($request);

   if ($response->is_success and $response->content_type =~ /text\/(plain|html)/ and not ($response->content_encoding || "") =~ /\S/ and ($request->header("accept-encoding") || "") =~ /gzip/) {
      require Compress::Zlib;
      my $content = $response->content;
      my $new_content = Compress::Zlib::memGzip($content);
      if (defined $new_content) {
         $response->content($new_content);
         $response->content_length(length $new_content);
         $response->content_encoding("gzip");
         warn("gzipping content from " . (length $content) . " to " . (length $new_content)) if $LOG_TRAN;
      }
   }

   $response;
}
}

Немного разоблачений:

  1. Код не мой, как видно по копирайту - я лишь подправил кое-что в области URI::URL, чтоб оно работало и добавил print $request->as_string для отладки.
  2. Переменную $HOST устанавливаем в адрес хоста, на котором крутится прокси, а $PORT присваиваем номер порта, который слушает наш прокси.
  3. Настраиваем клиент на использование прокси - указываем адрес хоста и порт $HOST:$PORT
  4. Скрипт при запуске плодит pre-forked потомков, которые слушают порт $PORT
  5. При поступлении запроса на соединение на указанный порт с помощью манипуляций объектами HTTP::Request, HTTP::Response и LWP::UserAgent перебрасываем запрос на сервер-рецепиент.

Вот так вкратце. А впрочем, из кода довольно прозрачно всё видно. Всё гениальное просто :) Можно запустить эту игрушку и шарить т.о. одно dial-up соединение для доступа к web (чем я сейчас и занимаюсь), не трогая NAT.

Saturday, June 2, 2007

Toshiba Satellite A100-811: Ubuntu & power management

Странное дело, но после апгрейда до KUbuntu 7.04 Feisty Fawn на ноутбуке сломался hibernate suspend-to-disk. Правка /etc/hibernate/common.conf ничего не давала. Процесс сброса образа RAM на диск, кажется, шёл вполне нормально, но после возобновления (resume) X оказывался запорот. Система не висит - в неё даже можно залогиниться по ssh, но работа непосредственно на ноутбуке невозможна. Итак, немного подумав, я открыл для себя uswsusp и входящий сюда s2disk. Однако, и тут не всё было гладко. Одно время s2disk работал для меня вполне прекрасно, даже с некоторыми особенностями. Так, например, после слива образа на диск и закрытия крышки лаптопа при её отркывании система включалась сама, без нажатия кнопки power и linux возобновлялся. Но, позже появилась проблема. При открытии крышки ноутбук включался, появлялся сплэш и... всё. Сплэш замерзал на одном месте и система не возобновлялась. Немного подумав, я полез шерстить по конфигам. Итак, найдя файл /etc/uswsusp.conf, я сделал такую правку:

# /etc/uswsusp.conf(8) -- Configuration file for s2disk/s2both 
resume device = /dev/sda5
compress = y
early writeout = y
image size = 512000000
compute checksum = y
RSA key file = /etc/uswsusp.key
shutdown method = platform
splash = y
Поясню: до этого параметр splash в конфиге не был определён вообще, а image size был равен 236Мб с копейками. Как видно, я явно установил splash = y, для фидбэка в процессе слива образа RAM на диск и в процессе его считывания с диска, а image size в пол-гигабайта (объём RAM на моём ноутбуке - 512Мб). Как ни странно, но после этого всё заработало. Скажу честно, я исходил из ни на чём не обоснованного предположения, что всё дело именно в том, что на диск сливается не весь образ памяти. Это было довольно интуитивное решение. Экспериментировать со значением 0 я не стал, но в man uswsusp.conf написано, что при нулевом значении s2disk будет пытаться сделать образ как можно меньше, сохраняя только самый необходимый минимум.

Теперь s2disk работал. И работал прекрасно. Единственное, чего мне ещё не хватало, это чтобы он срабатывал автоматически при критическом уровне батареи (такая настройка у меня была и ранее - в целях предотвращения потерь данных. Вполне вероятна такая ситуация, когда Вы отойдёте, задержитесь где-то, а тем временем аккумулятор благополучно иссякнет и...). К сожалению, отрабатывало оно так же, как и усыпление через диалог KDE "Завершить сеанс" -> "Спящий режим", или точнее сказать, не работало вовсе. Linux просто гасил экран, блокировал сеанс KDE, а засыпать даже не думал. Я пользуюсь KLaptop. Так сложилось традиционно, да и просто, он субъективно нравится мне больше всяких там KPowersave. Там вполне можно было прописать, какую команду следует выполнить при критически малом уровне аккумулятора, но мне хотелось универсального решения, ведь для кнопки в KDE'шном завершении сеанса такой параметр не прописан. Механизм работы здесь однако был один. Это я знал по опыту - по одинаковому поведению при попытке усыпить ноутбук через KLaptop и "Завершение сеанса". Немного пошарив по google я нашёл решение. Оказалось, что hibernation здесь срабатывает через hal. Для того, чтобы исправить ситуацию, необходимо было заменить модуль hal, который отвечал за усыпление по требованию клиента. Этот модуль представляет собой нечто иное, как скрипт, лежащий в /usr/lib/halscripts/linux - hal-system-power-hibernate-linux. Моей целью было заставить этот скрипт отрабатывать через s2disk. Первоначально скрипт выглядёл следующим образом:

#!/bin/sh

POWERSAVED_SUSPEND2DISK="dbus-send --system --dest=com.novell.powersave \
                         --print-reply /com/novell/powersave \
                         com.novell.powersave.action.SuspendToDisk"

unsupported() {
 echo org.freedesktop.Hal.Device.SystemPowerManagement.NotSupported >&2
 echo No hibernate script found >&2
 exit 1
}

#SuSE and ALTLinux only support powersave
if [ -f /etc/altlinux-release ] || [ -f "/etc/SuSE-release" ] ; then
 if [ -x /usr/bin/powersave ] ; then
         $POWERSAVED_SUSPEND2DISK
  RET=$?
 else
  unsupported
 fi

#Mandriva support suspend-scripts 
elif [ -f /etc/mandriva-release ] ; then 
    if [ -x /usr/sbin/pmsuspend ] ; then 
 /usr/sbin/pmsuspend disk 
 RET=$? 
    else 
 unsupported 
    fi 

#RedHat/Fedora only support pm-utils
elif [ -f /etc/redhat-release ] || [ -f /etc/fedora-release ] ; then
 if [ -x /usr/sbin/pm-hibernate ] ; then
  /usr/sbin/pm-hibernate
  RET=$?
 else
  unsupported
 fi

#Other distros just need to have *any* tools installed
else
 if [ -x "/usr/bin/powersave" ] ; then
         $POWERSAVED_SUSPEND2DISK
  RET=$?
 elif [ -x "/usr/sbin/pmi" ] ; then
  /usr/sbin/pmi action hibernate force
  RET=$?
 elif [ -x "/usr/sbin/pm-hibernate" ] ; then
  /usr/sbin/pm-hibernate
  RET=$?
 elif [ -x "/usr/sbin/hibernate" ] ; then
  # Suspend2 tools installed
  /usr/sbin/hibernate --force
  RET=$?
 elif [ -x "/sbin/s2disk" ] ; then
  # uswsusp tools installed
  /sbin/s2disk
  RET=$?
 elif [ -x "/etc/acpi/hibernate.sh" ] ; then
  # acpi-support installed
  /etc/acpi/hibernate.sh force
  RET=$?
 elif [ -w "/sys/power/state" ] ; then
  # Use the raw kernel sysfs interface
  echo "disk" > /sys/power/state
  RET=$?
 else
  unsupported
  fi
 fi

#Refresh devices as a resume can do funny things
for type in button battery ac_adapter
do
 devices=`hal-find-by-capability --capability $type`
 for device in $devices
 do
  dbus-send --system --print-reply --dest=org.freedesktop.Hal \
     $device org.freedesktop.Hal.Device.Rescan
 done
done

exit $RET

Из приведённого листинга видно, что сперва скрипт пытается определить дистрибутив, а далее действует исходя из реалий данного дистрибутива. В моём случае hal пытался отработать через pmi - абстактный интерфейс управления энергосбережением. Мне же нужно было, чтобы первым и последним срабатывал s2disk. После исправления скрипт обрёл такой вид:

#!/bin/sh

[ SKIPPED ]

#Other distros just need to have *any* tools installed
else
 if [ -x "/sbin/s2disk" ] ; then
  # uswsusp tools installed
  /sbin/s2disk
  RET=$?
 elif [ -x "/usr/sbin/pmi" ] ; then
  /usr/sbin/pmi action hibernate force
  RET=$?
 elif [ -x "/usr/bin/powersave" ] ; then
         $POWERSAVED_SUSPEND2DISK
  RET=$?
 elif [ -x "/usr/sbin/pm-hibernate" ] ; then
  /usr/sbin/pm-hibernate
  RET=$?
 elif [ -x "/usr/sbin/hibernate" ] ; then
  # Suspend2 tools installed
  /usr/sbin/hibernate --force
  RET=$?
 elif [ -x "/etc/acpi/hibernate.sh" ] ; then
  # acpi-support installed
  /etc/acpi/hibernate.sh force
  RET=$?
 elif [ -w "/sys/power/state" ] ; then
  # Use the raw kernel sysfs interface
  echo "disk" > /sys/power/state
  RET=$?
 else
  unsupported
 fi
fi

[ SKIPPED ]
После этого и KLaptop и "Завершение сеанса" работают безупречно.

Кстати, режим suspend-to-ram у меня работал хорошо из коробки (после апгрейда до Feisty Fawn). s2ram напротив работать не хочет, ссылаясь на то, что модель ноута ему не знакома. Поэтому мои настройки энергосбережения закончились доведением до ума режима suspend-to-disk a.k.a. hibernation.

"Вот так мы победили сырость!" (с) Однако, есть ещё над чем поработать. Так, например, мне до сих пор не удалось заставить работать русский в консоли. Самое странное, что на десктопе с установленным Debian 4.0 Etch всё прекрасно работает (console-cyrillic не установлен). При такой же конфигурации на лаптопе оно не работает. Странность в том, что из коробки путём лишь небольших манипуляций после замены дистрибутивного ядра на собственное русский текст в UTF8 отображается в консоли без проблем, но нет возможности набирать русский текст. Вместо него не набирается вообще ничего. Одним словом, проблема всё ещё требует решения. В Edgy Eft в этом плане всё было отлично. Примечательно, что такие проблемы после перехода на Feisty Fawn наблюдаются не только у меня.

Вторая проблема более странная. С ней я пока не разбирался вплотную. Я наконец решил заставить работать встроенный модем. Поставил sl-modem-daemon, sl-modem-source для компиляции модулей под моё кастом-билт ядро. Но тут я столкнулся с тем, что sl-modem-source на уровне исходного кода не совместим с новыми ядрами. К примеру, в коде kernel-ver.c делается попытка подключить заголовок linux/version.h чтобы использовать макрос UTS_RELEASE, который в 2.6.20 (и не только в нём) уже определяется в другом файле - linux/utsrelease.h. Можно, конечно, и вручную подправить сырцы драйвера... Кстати, смешной момент, связанный с установкой драйвера. Если сказать apt-get install sl-modem-source, то в Вашем /usr/src появится тарбол sl-modem.tar.bz2 - это и есть сырцы драйвера. Далее, пытаемся ставить его с помощью module-assistant (хех, Debian-way): module-assistant prepare sl-modem; module-assistant install sl-modem и тут происходит ошибка. По вышеупомянутой причине. Правим сырцы, устраняем явные противоречия и снова пускаем m-a install sl-modem и... снова ошибка! В том же месте! :) Лезем в исходники и видим, наших изменений там нет! Так что, мораль в том, что если Вы собираетесь править сырцы sl-modem вручную, то Вам нужно будет из подправленных сырцов создать такой же тарбол с тем же именем, что и оригинал и заменить оригинал. Правда, я пошёл по другому пути - просто выкачал sl-modem-source из пула Debian Sid. Всё собралось без сучка и задоринки, но slmodemd всё равно не видит модули, хотя они и установлены в /lib/modules/`uname -r`/misc. Говорит: FATAL: Module slamr not found. Странно, короче. Можно было бы обойтись без модулей, используя alsa, но это, ИМХО - крайний случай.

ПОСЕТИТЕЛИ

free counters