Advanced пейджер
 
Исходный код: lanpager-0.1.zip (2003-01-04 12:04:57/1620/229)

Как я и обещал, эпопея с админским пейджером не закончена. Если хотите, то предыдущая статья была только началом - разогревочным этапом, позволяющим прощупать почву и найти более выгодное решение. Какие выводы можно сделать, проанализировав попытки довести до ума первый вариант программы? Прежде всего хочется заметить крайне отвратительное функционирование сигналов в среде Win32. Кроме того, особенности реализации perl для этой платформы могут запутать не очень внимательного программиста. В общем, полноценную мультиплатформенную реализацию разработать довольно трудно. Поэтому придется разработать два вариант программы: для операционных систем семейства Windows и для UNIX-подобных осей. Вариант для UNIX-а написать легче, так как большиство документации ориентированно именно на UNIX и те аспекты, которые вызвали затруднения под Windows, очень легко реализуются под UNIX. А мы пока остановимся на реализации версии для Win32.

Изменения в схеме работы

Прежде всего, хочу отметить что этот вариант работает совершенно по другой схеме. Во-первых, если в предыдущем варианте пейджера основной процесс содержал код сервера, а пищалка и обработка ввода выполнялась в отдельных дочерних процессах, то в этой реализации в основном пороцессе выполняется код обработчика ввода. Большинство функций ввода сложнее getc не реализованы под Windows. Поэтому обработку ввода пришлось возложить на модуль Win32::Console. Этот модуль входит в стандартную поставку ActiveState Perl, поэтому проблем с его отсутствием быть не должно. А если вы собственноручно собирали perl, то тем более для вас установка какого-то там Win32::Console не представляет большой проблемы. Итак, это первая причина, по которой настоящая реализация не может быть использована на платформе UNIX.

Сам сервер теперь вынесен в отдельный дочерний поток. И это правильно, так сервер по сути выполняет автономную задачу - только читает данные от клиентов и выводит их на экран. Все управление сервером сводится к его уничтожению. Таким образом, мы повышаем читабельность программы и четко определяем схему работы: между пользователем и сервером, как и положено, находится интерфейс, который может не только управлять сервером, но еще выполнять и другие задачи. Какие другие? Ну например - выводить справку по горячим клавишам или отключать пищалку.

Еще одним немаловажным изменением (или даже дополнением) является вынос кода пищалки в пространство абсолютно автономного процесса. То есть, теперь в программе три процесса, которые явно между собой не связаны. А так как сигналы в среде Win32 глючат, то для управления этой кучей процессов пришлось воспользоваться чисто виндовой фичей под названием event. Что это за птица, мы конечно разберемся, но по сути это еще одно ограничение, которое не позволяет использовать пейджер под UNIX-ом.

Ко всему прочему, я все таки дописал код функции Log. Очень советую обратить внимание на эту функцию, так как она представляет собой прекрасный образчик эффективного (в плане скорости разработки) программирования. Так как писать ее мне было лень, я, по совету Ларри, использовал первое что пришло в голову - при индикации переполнения лог-файла читать строки в массив, удалять по строке (как FIFO), пока массив не достигнет допустимых размеров, и сохранить все это в лог-файл. Так же в коде этой функции можно наблюдать поразительно наплевательское (на первый взгляд) отношение к дескрипторам файлов. Там они открываются, переоткрываются и пере-переоткрываются несколько раз. Однако, от этого может только покоробить программиста. А на самом деле все работает правильно. И если мы обратимся к документации по функции open, то мы узнаем, что open автоматически затыкает хэндл перед повторным его открытием.

Вот такие, можно сказать революционные, изменения претерпел наш простенький давеча пейджер. Так давайте же скорее взглянем на сие произведение.

Административная работа

Как я уже говорил, в основном процессе выполняется код обработки ввода. Выглядит это, немного-немало, так (кстати, эту версию я обозвал крутым названием lanpager.pl)

#!/usr/bin/perl -w # lanpager.pl - advanced pager use strict; use Socket; use Win32::Console; use Win32::Event; my ($host,$port,$logf,$maxl) = ('localhost',65000,'pager.log',10240); $| = 1; # Создаем event для регулирования пищалки my $bzz_event = Win32::Event->new(1,0,"Pager_bzzz_event") or 	Log("Can't' create event: $!\n")&¨ # Запускаем обработчик пищалки	 my $bzz_pid = RunBzzzController() or 	Log("RunBzzzController failed: $!\n")&¨ # Стартуем сервер my $server_pid = StartServer() or Log("Can't start server: $!\n")&¨ # Создаем объект для обработки ввода my $cons = Win32::Console->new(STD_INPUT_HANDLE) or 	Log("Keydev creation failed: $!\n ")&&Destroy(); # Рабочий цикл print "Welcome to LAN-pager. Press h key for view help page.\n"; while (1){ my @evt = $cons->Input() or next; 	next if $evt[0] != 1; 	next unless $evt[1]; 	next if $evt[5] == 0; 	# Если звенит, то вырубаем 	$bzz_event->reset; 	my $char = lc(chr($evt[5])); 	if ($char eq 'q' || $char eq 'e'){ 		# Shutdown pager 		print "Ok, exiting...\n"; 		last; 	}elsif ($char eq 'h' || $char eq '?'){ 		print "Pager hot-keys:\n"; 		print "q,e\t- shutdown and quit\n"; 		print "h,?\t- this help\n"; 		print "other\t- disable skreeper signal\n"; 	} 	sleep 1; } Destroy(); 

Вот как должна выглядеть настоящая программа в ее административной части: только шаги инициализации и основной цикл. Итак - по порядку. Первым делом с помощью переменной $| мы заставляем буффера вывода сбрасываться после каждой операции вывода. Это правильно, так как у нас несколько процессов и они что то печатают. Будет плохо, если данные одного процесса застрянут в буффере, а в это время другой процесс будет что-то выводить - получится глюк и вообще по-ламмерски.

Далее, мы создаем объект event. Заострять внимание на нем не сейчас мы будем - позже я объясню более детально. Однако именно сейчас нужно вспомнить о том, что каждый из дочерних процессов, которые мы наплодим чуть позже, получать копию этой переменной. Таким образом мы избавляемся от лишних телодвижений при инициализации этого объекта. В последствии каждый процесс может использовать свой экземпляр по усмотрению.

Следующим шагом является инициализация пищалки. Эта функция порождает процесс, который будет отвечать за своевременную подачу звукового сигнала. Похвастаюсь заранее - этому контроллеру со смешным названием абсолютно пофиг те причины, по которым он должен пищать - ему сказали он и пищит. Ему сказали заткнись - он заткнулся. Это, опять-таки, плюс в нашей схеме и хороший пример для подражания. Отдельный процесс занимается отдельной задачей. Как работает бзик-контроллер мы то же разберем позже.

Далее следует вызов функции запуска сервера. Туда переехал почти весь код из предыдущей реализации пейджера. Помимо этого, там добавился код, отделяющий сервер в отдельный процесс.

Для безгемморойной работы с потоком ввода мы будем использовать Win32::Console. Посему, следующим шагом является создание объекта этого класса.

Мы подходим к ключевому моменту схемы - рабочему циклу. Но перед этим надо же понтануться и напечатать что нибуть в качестве приветствия.

Рабочий цикл базируется на получении данных с STDIN. Исчерпывающую информацию, готовую к употреблению в пищу, нам предоставляет метод Input() класса Win32::Console. Этот метод возвращает массив, описывающий произошедшее событие: ввод с Клавы или Миши. Пока на вводе ничего нет, Input() блокирует программу. Кстати, нужно заметить, что по дефолту метод Input() не будет обрабатывать Мишкины события. Для этого нужно малеха проманипулировать с объектом консоли но об этом читайте в документации - там все очень просто. Input() возвращает нам список следующих данных:

  • Тип события - для Клавы равен 1, для Миши - 2. В принципе, проверку, которая стоит первой в блоке цикла, можно убрать, так как мы не просили слушать Мишу. Но, черт его знает, пусть остается.
  • Истина - клавиша была нажата, ложь - отпущена.
  • Количество повторений - должно быть понятно, а если и не понятно - нас здесь не интересует.
  • VK или код виртуальной клавиши. Это чисто виндовозовская привычка, все делать по своему. Каждой клавише сопоставлено определенное константное значение (при чем эскейпам то же).
  • Скан код клавиши.
  • ASCII-код (ё-моё сколько кодов то) для тех, которые представляют собой обычные клавиши или 0 для эскейпов.
  • Статус контрольных клавиш, таких как Ctrl, Shift, Alt. Если вас они интересуют, то там же в документации есть описание констант, с помощью которых можно определить состояние всех этих клавиш.
Ну вот, теперь, когда мы разобрались с тем, что нам подсовывает метод Input(), с оставшимся кодом проблем быть не должно, за исключением того оператора, где юзается объект event. Давайте же наконец разберемся что это за птица.

Что такое event

Не спешите лезть в перловую документацию - там вы мало чего найдете про event. Я бы и сам не обратил на него внимания, если бы до этого сидел на сях в UNIX-ах. Там такого нет, и, честно говоря, не надо. Объект event должен быть известен сишникам-виндовозникам (это такой вид программистов, если кто не знает). Так вот, объект event, наряду с другими глюками виндовоза, предназначен для торможения системы :) , иначе говоря - IPC. Давайте попробуем разобраться на жизненных аналогах.

Представте себе водопроводную трубу по которой течет вода. Течет себе и течет, и вдруг какой нибудь умник врезает в трубу вентель. Когда вентель открыт - вода течет как будто так и надо. А когда закрыт, соответственно, не течет. Вот event в Win32 тот же вентиль. Но, как это ни странно, вентиль этот не на трубе находится (однако). Да-да, он находится между трубами. И любая труба может проверить - а не закрыт ли кранчик? При чем труба сама может решать - а вообще смотреть мне на этот вентиль, или пошел он нафиг. И это еще не все. Как вы думаете, кто крантик крутить должен? Сантехник? А вот и нет. Любая труба может и открывать и закрывать кран (ну и воображение у Борьки). Вот если согласно этой аналогии вентилем будет объект event, тогда трубу будет представлять отдельный процесс (в действительности это может быть и поток - нить на мультиплатформонезависимом диалекте).

Если вас не устраивает такое объяснение, то могу посоветовать почитать более серьезную литературу. У Петьки Нортона, например, есть двухтомник юзания MFC (фу, как меня не стошнило), в котором больмень подробно описывается когда и к каким блюдам должны подаваться event-ы.

Теперь вы хоть в общих чертах да имеете представление об объектах event. Пора приступить к разбору полетов, то есть того, каким образом event используется в нашем пейджере.

Как мы используем event

Представим, как должна пищать наша пищалка. Пищать она должна тогда, когда сервер обрабатывает входящее подключение, то есть когда у нас появляется новое сообщение на пейджере. А когда она не должна пищать? Правильно, по результатам предварительной экспертизы тогда, когда админ нажмет эникей. А теперь призадумаемся - Хм, а как это мы будем управлять пищалкой, если сервер у нас в одном процессе, а обработка ввода в другом. Наверное наша схема никуда не годится и нужно было и ввод и подключения обрабатывать в одном процессе. Нет, ребята, скажу я вам, вы не правы. Сервер при акцепте блокирует процесс? Блокирует. А ожидание ввода блокирует? Блокирует. Вы что же хотите возиться с четырехаргументным селектом? Не думаю. Но ведь у нас есть крантик, который мы можем открывать и закрывать из любой трубы-процесса. Ну так чего ж мы ждем?!

sub RunBzzzController{ 	my $pid = fork; 	return undef unless defined $pid; 	return $pid if $pid != 0; 	while ($bzz_event->wait()){ 		print "\a" and sleep 1; 	} 	exit; } 

Настало самое время разобраться с функцией бзик-контроллера. С самого начала мы ветвимся и возвращаем идентификатор ответвленного процесса. Идентификатор мы возвращаем для того, чтобы как правильные программисты в последствии корректно пришибить бзик-процесс. И что дальше? Вот эти три строчки и есть наш звонок? Да! Это и есть наш звонок. Правда здорово? А теперь представьте, что бы получилось у нас в случае использования селекта с четырмя аргументами - код увеличился бы неимоверно.

Итак, опять на аналогах. Когда кран открыт (академическим языком выражаясь - объект event установлен в сигнальное состояние), водичка течет себе, перетекает через цикл, выполняя при этом один писк скриппером и возвращается на круги своя. А когда кран закрыт и водичка не течет (опять же академически - объект event сброшен), мельница не работает. Колесо застревает на выполнении метода wait, который без аргумента блокирует выполнение процесса до тох пор, пока event не будет установлен в сигнальное состояние. Ну, здесь остается добавить, что переменная $bzz_event досталась нам в наследство от родительского процесса. Если бы мы не позаботились об этом раньше, нам бы пришлось заново создавать, инициализировать, и т.д. и т.п.

А сейчас давайте вернемся к оператору создающему объект evant и мы узнаем, почему и как при запуске наш кран оказывается закрытым.

my $bzz_event = Win32::Event->new(1,0,"Pager_bzzz_event") or... 

Взгляните на этот код. Именно в аргументах заключается все шаманство. Первый аргумент (он булева типа) говорит о том, что наш event будет мануально-сбрасываемым. Это значит, что после установки в сигнальное положение, сброс должен быть выполнен программным путем. Нам как раз такое свойство и нужно, поэтому мы передаем в качестве аргумента значение истина. Следующий аргумент управляет начальной инициализацией события. Это тоже булево значение и в нашем случае оно представлено ложью - то есть заставляет event быть сброшенным (кран закрыт) после инициализации. Третий аргумент представляет собой строку, ассоциированную с объектом - имя объекта. Благодаря этому имени мы вообще можем обращаться к событию не только из другого процесса, но и из другой программы (возвращаясь к юморной аналогии, управлять вентелем могут и трубы в соседнем помещении). Для нас имя не имеет значения. Здесь остается добавить, что если в качестве имени передается имя уже существуещего объекта, то выполняется не создание, а открытие события. То есть, будет использоваться то событие, которое было создано ранее. В такой ситуации первые два аргумента игнорируются.

Вспомним, кто у нас должен открывать кран. Правильно, сервер в момент получения входящего соединения. Давайте посмотрим на эту функцию.

sub StartServer{ 	my $sock_name = GetSockName($host,$port) or return undef; 	socket(SERVER,PF_INET, 		SOCK_STREAM,getprotobyname('tcp')) or return undef; 	setsockopt(SERVER,SOL_SOCKET,SO_REUSEADDR,1) or return undef; 	bind(SERVER,$sock_name) or return undef; 	listen(SERVER,SOMAXCONN) or return undef; 	my $parent_pid = $$;	# Main process 	my $pid = fork; 	close(SERVER)&&return undef unless defined($pid); 	close(SERVER)&&return $pid if $pid != 0; 	 	# Server code 	Log("Pager started\n"); 	while (1){ 		my $rem_addr = accept(CLIENT,SERVER); 		my ($ip,$pt) = GetSockAddr($rem_addr); 		Log("Connection from $ip:$pt\n"); 		$bzz_event->set; 		Log($_) while <CLIENT>; 		close(CLIENT); 	} } 

Мы видим немного видоизмененный код предыдущего пейджера. Его мы рассматривать не будем (мы ведь договаривались). А интересует нас здесь всего один оператор внутри цикла обработки входящих подключений

		$bzz_event->set; 

Мы вызываем метод set() объекта event устанавливая объект в сигнальное состояние и, тем самым, открывая наш кран. Вода начинает течь а бзикер пищать. Что и требовалось получить. И тут замечу, что $bzz_event достался по наследству от родительского процесса.

Теперь вернитесь к управляющему циклу обработки ввода и убедитесь что вовремя вызывается метод reset() объекта event, который сбрасывает объект и закрывает кран. Как показала практика, клавиатура не выдерживает прицельных попаданий по клавишам во время гнева админа. По этому, сброс выполняется при получении любого(что б быстрее отстал) ввода с клавиатуры.

А на последок я скажу

У нас осталось еще две функции, которые на самом деле не заслуживают пристального внимания.

sub Destroy{ 	kill INT => $server_pid;		# Грохаем сервер. 	kill INT => $bzz_pid if $bzz_pid;	# Затыкаем пищалку. 	waitpid $server_pid,0;			# Ожидаем пока не 	waitpid $bzz_pid,0;			# заткнутся оба. 	print "Shutdown pager\n"; 	exit;					# Готово! } 

Функция Destroy() напомнила мне времена, когда я программировал под WinAPI. Меня так и подмывало поставить префикс, что бы получилось OnDestroy().

Функцией Log() я уже похвастался выше и на данный момент у меня нет никаких причин сомневаться, что вы разберетесь с ее кодом.

sub Log{ 	return unless defined $_[0]; 	my $t = scalar(localtime)." ".$_[0]; 	print $t; 	open F,">$logf" unless -f $logf; 	print "Can't open log file: $!\n" and return 		unless open F,">>$logf"; 	print F $t; 	if (-s $logf > $maxl && open F,$logf){ 		my @lines = <F>; 		my $s = join("",@lines); 		while (length($s) > $maxl){ 			shift(@lines); 			$s = join("",@lines); 		} 		print F $s if open F,">$logf"; 	} 	close F; } 
Ну вот и все. Следующим шагом я предполагаю хорошенько протестировать пейджер каким нибудь серьезным заданием.
 
Автор: Whirlwind
 
Оригинал статьи: http://woweb.ru/publ/58-1-0-422