воскресенье, 27 января 2013 г.

Генераторы и сопрограммы Python. Часть 2

Эта заметка является идейным и фактическим продолжением одной из моих прошлых заметок - Генераторы и сопрограммы Python. На сей раз я продолжу борьбу за элегантность исходного кода, продолжая приносить в жертву лёгкость понимания средств, лежащих в основе этой элегантности :)

На сей раз мы ещё немного усложним задачу.

Во-первых, хочется замаскировать отправку значений в сопрограмму так, чтобы это выглядело как простой вызов функции, а не вызов метода объекта.

Во-вторых, теперь перед добавлением записи нам нужно проверить, существует ли она. Если запись уже существует, то её не нужно добавлять.

Для решения первой задачи сначала мне пришло в голову такое решение - сделать дополнительный класс-обёртку с методом __call__:
class wrapped_coro():
    def __init__(self, coro):
        self.coro = coro

    def __call__(self, *args, **kwargs):
        return self.coro.send(*args, **kwargs)

class wrapper():
    def __init__(self, coro, *args, **kwargs):
        self.coro = coro(*args, **kwargs)

    def __enter__(self):
        self.coro.next()
        return wrapped_coro(self.coro)

    def __exit__(self, type, value, traceback):
        self.coro.close()
        if value is None:
            return True
        return False
Однако, немного подумав, я понял, что это ничем не оправданный оверинжениринг, удалил класс wrapped_coro и переписал wrapper вот так:
class wrapper():
    def __init__(self, coro, *args, **kwargs):
        self.coro = coro(*args, **kwargs)

    def __enter__(self):
        self.coro.next()
        return self.coro.send

    def __exit__(self, type, value, traceback):
        self.coro.close()
        if value is None:
            return True
        return False
Получилось даже ещё проще, чем в прошлой заметке. Теперь функцию копирования можно переписать так:
def copy(db):
    """
    Подпрогрмма, использующая генератор и сопрограмму для копирования
    содержимого таблицы user в таблицу user2
    """
    with wrapper(writer, db) as write:
        for row in reader(db):
            write(row)

Для решения второй задачи воспользуемся ещё одной возможностью, предоставляемой оператором yield - он может не только возвращать значение или только считывать его, но и считывать и возвращать одновременно. Делается это так:
def checker(db):
    """Сопрограмма. Проверяет, что указанный пользователь уже добавлен в таблицу users2"""
    select = db.cursor()
    exists = None
    try:
        while True:
            row = (yield exists)
            select.execute('''SELECT COUNT(*)
                              FROM user2
                              WHERE surname = %s
                                AND name = %s
                                AND patronym = %s''', row)
            count, = select.fetchone()
            exists = count != 0
    except GeneratorExit:
        select.close()
Перед началом использованием сопрограммы, как и прежде, нужно прокрутить её до первого оператора yield. Делается это, как и прежде, вызовом метода next из обёртки.

Новый вариант функции копирования примет следующий вид:
def copy(db):
    """
    Подпрогрмма, использующая сопрограммы для дополнения таблицы user2
    содержимым таблицы user
    """
    with wrapper(writer, db) as write:
        with wrapper(checker, db) as check:
            for row in reader(db):
                if not check(row):
                    write(row)
Читается, на мой взгляд, хорошо - код компактен и его логика легко просматривается, если понимать, для чего нужен wrapper. Полностью программа теперь будет выглядеть так:
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

def reader(db):
    """Генератор. Читает строки таблицы user"""
    select = db.cursor()
    select.execute('SELECT surname, name, patronym FROM user')
    for row in select:
        yield row
    select.close()

def writer(db):
    """Сопрограмма. Пишет строки в таблицу user2"""
    insert = db.cursor()
    try:
        while True:
            row = (yield)
            try:
                insert.execute('INSERT INTO user2(surname, name, patronym) VALUES(%s, %s, %s)', row)
                db.commit()
            except:
                db.rollback()
    except GeneratorExit:
        insert.close()

def checker(db):
    """Сопрограмма. Проверяет, что указанный пользователь уже добавлен в таблицу users2"""
    select = db.cursor()
    exists = None
    try:
        while True:
            row = (yield exists)
            select.execute('''SELECT COUNT(*)
                              FROM user2
                              WHERE surname = %s
                                AND name = %s
                                AND patronym = %s''', row)
            count, = select.fetchone()
            exists = count != 0
    except GeneratorExit:
        select.close()

class wrapper():
    def __init__(self, coro, *args, **kwargs):
        self.coro = coro(*args, **kwargs)

    def __enter__(self):
        self.coro.next()
        return self.coro.send

    def __exit__(self, type, value, traceback):
        self.coro.close()
        if value is None:
            return True
        return False

def copy(db):
    """
    Подпрогрмма, использующая сопрограммы для дополнения таблицы user2
    содержимым таблицы user
    """
    with wrapper(writer, db) as write:
        with wrapper(checker, db) as check:
            for row in reader(db):
                if not check(row):
                    write(row)

db = MySQLdb.connect(user = 'user',
                     passwd = 'p4ssw0rd',
                     db = 'database',
                     charset = 'UTF8')

copy(db)

db.close()

воскресенье, 20 января 2013 г.

Распаковщик и упаковщик игровых ресурсов PUP

Предыстория.

В начале 2000-х годах я любил ковыряться в играх и доставать из них различные ресурсы - графику, звуки, модели. Моими главными и единственными инструментами тогда были шестнадцатеричные редакторы pview и hiew, компиляторы Pascal и C/C++. Наковырял я довольно много, правда тогда мне было интересно только доставать из игр ресурсы, но не делать инструменты для редактирования.

Некоторые форматы файлов были проанализированы поверхностно - на уровне, достаточном для извлечения данных, но не более. Некоторые были проанализированы довольно хорошо, так что я даже смог реализовать не только извлечение данных, но и обратную упаковку.

Спустя некоторое время я погряз в дебрях ООП, так что на проектирование объектной структуры программы стало уходить столько времени, что я не мог довести до конца ни одной программы. Это было началом "творческой депрессии", в результате которой я надолго забросил программирование.

История.

В сентябре 2010 года я решил тряхнуть стариной и написать программу для распаковки и упаковки файлов с игровыми ресурсами. Чтобы не повторять прошлых ошибок, писать я решил на чистом C, без использования ООП. Я спроектировал плагинную систему и решил реализовать в этой программе поддержку только тех форматов, которые хорошо на неё ложатся.

К февралю 2011 года я практически завершил разработку программы, планируя добавить поддержку ещё одного формата (IWAD/PWAD). С этим форматом возникли сложности и работа застопорилась. Хотя я периодически вспоминал о программе и пытался доделать поддержку этого формата, в конце концов я нашёл какое-то более интересное занятие и забыл об этой программе.

Недавно я о ней вспомнил и подумал - не пропадать же добру, может кому пригодится. Попробовал собрать, исправил пару ошибок, из-за которых сборка не шла, удалил зачатки неудавшегося плагина и решил выложить всё это.

Поскольку к 2010 году я уже пользовался Debian'ом, то программа тоже делалась на нём. Сборка осуществляется с помощью shell-скрипта, для сборки нужны пакеты gcc, zlib1g, zlib1g-dev. Сборка в других системах не тестировалась.

Кстати, большинство игр прекрасно работает в wine и dosbox'е. Все я не тестировал, но думаю, что на самом деле работают все.

Программа.

Программа называется PUP - Packer/UnPacker (слово pup также можно перевести с английского как "щенок"). Программа имеет несколько режимов работы:

1. Если программа запущена без опций, выводится справка по доступным опциям.

2. При указании опции --list программа выводит описание всех доступных плагинов.

3. При указании опции --unpack, программа попытается определить формат указанного ей файла и распакует его содержимое в указанный каталог, а метаданные файла сохранит в указанный файл.

Если имя файла метаданных не указано, используется имя каталога с добавленным к нему расширением ".txt".

Если имя каталога не указано, используется имя файла без расширения.

Чтобы программа не пыталась определить формат файла самостоятельно, можно указать опцию --plugin с указанием определённого формата.

4. При указании опции --pack, программа запакует данные в указанный файл из указанного каталога с использованием метаданных из указанного файла.

Если имя файла метаданных не указано, используется имя каталога с добавленным к нему расширением ".txt".

Если имя каталога не указано, используется имя файла без расширения.

При упаковке обязательно указать опцию --plugin с указанием формата формируемого файла.

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

Если имя файла метаданных не указано, используется имя исходного файла с расширением, заменённым на ".txt".

Чтобы программа не пыталась определить формат файла самостоятельно, можно указать опцию --plugin с указанием определённого формата.

6. При указании опции --print, программа выводит на стандартный вывод техническую информацию из каталога ресурсов указанного файла (смещение ресурса, его сжатый и исходный размер, название ресурса и т.п.).

Чтобы программа не пыталась определить формат файла самостоятельно, можно указать опцию --plugin с указанием определённого формата.

Справка программы:
$ ./pup
Usage: pup --list
       pup --plugin <plugin> --pack <file> [<dir> [<meta>]]
       pup [--plugin <plugin>] --unpack <file> [<dir> [<meta>]]
       pup [--plugin <plugin>] --savemeta <file> [<meta>]
       pup [--plugin <plugin>] --print <file>
Options:
       --plugin <plugin>   - specify certain plugin
Modes:
       --list     - list of all supported plugins
       --pack <file> [<dir> [<meta>]]  - packing dir to specified file
       --unpack <file> [<dir> [<meta>]]  - unpacking specified file to dir
       --savemeta <file> [<meta>]  - only save metadata to specified metafile
       --print <file>    - print technical information to stdout
Список поддерживаемых форматов:
$ ./pup --list
Supported plugins:
grp     GRP-files of Duke Nukem 3D, Witchaven, Redneck Rampage, Shadow Warrior
gob     GOB-files of Star Wars: Dark Forces
pak     PAK-files of Dune II: The Building of a Dynasty
viv     VIV-files of the Need For the Speed 3: Hot Pursuit
vpp     VPP-files of Red Faction, The Punisher, Summoner
pack    PAK-files of Quake, Quake II, Half-Life, Heretic 2, MDK 2
pack2   PAK-files of Daikatana
wad2    WAD-file of Quake
wad3    WAD-files of Half-Life
res     RES-file of Comanche 3
dpk4    DPK-file of Starmageddon 2
dat     DAT-files of Fallout
dat2    DAT-files of Fallout 2
rff20   RFF-files of Blood, version 2.0
rff30   RFF-files of Blood, version 3.0
rff31   RFF-files of Blood, version 3.1
Программа отлично справляется с определением типа исходного файла, поэтому используемый плагин указывать совершенно не обязательно. Плагин нужно указывать только при упаковке, чтобы программа сформировала файл нужного вам формата.

Метаданные используются только в формате RFF. Метаданные - это дополнительные информационные поля, сопровождающие каждый упакованный файл. Эти поля никак нельзя восстановить по содержимому самого файла, поэтому при распаковке RFF-файла нужно сохранить эти поля в текстовый файл, а при упаковке - загрузить их из текстового файла.

Есть много различных упаковщиков и распаковщиков файлов игр. Уникальность моей программы заключается в поддержке форматов pak-файлов Daikatana, res-файлов Comanche 3 и rff-файлов Blood.

Формат pak-файлов Daikatana и его алгоритм компрессии я анализировал сам. Сам же написал декомпрессор, что было довольно легко, и компрессор, что оказалось значительно сложнее. Я не силён в алгоритмах сжатия, поэтому компрессор работает довольно медленно, однако сжимает он лучше, чем компрессор разработчиков. Распаковка и упаковка была реализована 15 сентября 2002, а более эффективная версия упаковщика - 20 октября 2002.

Формат res-файлов Comanche 3 я тоже анализировал сам. Сложность анализа этого формата заключалась в том, что имена ресурсов в нём были зашифрованы, так что просто глазами найти имена файлов оказалось непросто.

Тогда у меня была самописная программа, которая позволяла находить в произвольном файле содержащиеся в нём файлы определённых форматов по их сигнатурам. Например, wav-файлы и avi-файлы находились по сигнатуре RIFF (плюс дополнительные проверки), PCX-файлы находились по характерной для них сигнатуре и т.п. С помощью этой программы я нашёл в res-файле Comanche 3 точки начала нескольких файлов. Рядом с ними я увидел их размеры и какие-то смещения. И ещё были поля с непонятным содержимым. Я написал программу, которая выводила мне каталог всех ресурсов и смогла их распаковать в файлы, имена которых составлялись из порядкового номера ресурса.

Дальше я заметил в тех непонятных записях, что последние байты часто бывают одинаковыми и сделал предположение, что это - результат шифрования байта со значением 0 операцией XOR. Я подумал, что в конце имени файла обычно бывает его расширение и нашёл ресурсы, формат которых мне известен. Так я восстановил четырёхбайтовый ключ шифрования, который для шифрования 12-байтового имени файла использовался трижды. Первая программа была написана 09 февраля 2003 и умела только распаковывать ресурсы.

С rff-файлами Blood всё оказалось и проще и сложнее. Информацию об алгоритме шифрования файлов я нашёл в Интернете, однако она подходила не ко всем найденным мной файлам rff. Я нашёл несколько разных версий утилиты BARF для создания новых файлов для Blood и попробовал сформировать ими новые файлы. Так я нашёл три разных формата rff-файлов, распаковку и упаковку которых и реализовал. Однако, игра не захотела работать с родными файлами, обработанными распаковкой и упаковкой. В конце концов я реализовал сохранение и загрузку метаданных rff-файлов при их распаковке и упаковке соответственно. Приём сработал и Blood стал работать с пересобранными файлами.

С остальными форматами всё было проще. Некоторые я проанализировал сам, о некоторых почитал в интернете.

Архив с исходными текстами программы можно скачать здесь. Там же есть текстовый файлик с именем ideas.txt, в котором я записывал идеи и отмечал этапы разработки программы.

воскресенье, 13 января 2013 г.

Nouveau + Xrandr: Монитор и телевизор

В прошлом я уже описывал два способа одновременного использования монитора и телевизора на одной видеокарте: Xinerama: монитор и телевизор и TwinView: монитор и телевизор.

На этот раз мне, по некоторым причинам, понадобилось поставить свободный драйвер Nouveau. Сам по себе он ставится довольно просто, а вот с настройкой одновременного использования монитора и телевизора пришлось попотеть.

Установка nouveau

Итак, сначала опишу процедуру замены проприетарного драйвера nvidia на свободный nouveau. Для этого я воспользовался статьёй Переход на драйвер nouveau с проприетарных драйверов nvidia.

У меня установлен Wheezy, поэтому я сразу установил драйвер (в ответ на что система сообщила, что у меня уже установлено всё необходимое):
# apt-get install libdrm-nouveau1a xserver-xorg-video-nouveau
Остановил LightDM из первой консоли:
# /etc/init.d/lightdm stop
Выгрузил модули nvidia и drm и загрузил модуль nouveau:
# rmmod nvidia
# rmmod drm
# modprobe nouveau
Удостоверился, что система не ругнулась в ответ на загрузку модуля nouveau:
# dmesg | tail
Сделал резервную копию файла конфигурации X-сервера:
# cp /etc/X11/xorg.conf /etc/X11/xorg.conf-nvidia-twinview
В исходном файле закомментировал секции Monitor и Screen, соответствующие телевизору, в секции ServerLayout оставил только один Screen, в секции Driver закомментировал использование TwinView.
Затем удалил пакет nvidia-glx:
# apt-get remove nvidia-glx
И поставил замену для него:
# apt-get install libgl1-mesa-dri libgl1-mesa-glx
Затем попробовал запустить LightDM и убедился, что монитор работает:
# /etc/init.d/lightdm start
Затем, удалил остатки проприетарного драйвера nvidia:
# apt-get remove nvidia-\*
Добавил модуль проприетарного драйвера в чёрный список, закомментировав строчку с nouveau и прописав строчку с nvidia в файле /etc/modprobe.d/nvidia-kernel-common.conf:
#blacklist nouveau
blacklist nvidia
И добавил модуль nouveau в автозагрузку:
# echo nouveau >> /etc/modules
Теперь настала пора самого сложного - настроить телевизор.

Настройка монитора и телевизора

В результате всех мытарств получился такой /etc/X11/xorg.conf:
Section "ServerFlags"
    Option "AutoAddDevices" "False"
    Option "AllowEmptyInput" "False"
    Option "DontZap" "False"
EndSection

Section "ServerLayout"
    Identifier     "X.org Configured"
    Screen      0  "Screen0" 0 0
    InputDevice    "Mouse0" "CorePointer"
    InputDevice    "Keyboard0" "CoreKeyboard"
EndSection

Section "Files"
    ModulePath   "/usr/lib/xorg/modules"
    FontPath     "/usr/share/fonts/X11/misc"
    FontPath     "/usr/share/fonts/X11/Type1"
    FontPath     "/var/lib/defoma/x-ttcidfont-conf.d/dirs/TrueType"
EndSection

Section "Module"
    Load  "dri"
    Load  "GLcore"
    Load  "glx"
    Load  "dbe"
    Load  "record"
    Load  "extmod"
    Load  "xtrap"
    Load  "Xrandr"
EndSection

Section "InputDevice"
    Identifier  "Keyboard0"
    Driver      "kbd"
    Option      "XkbRules" "xorg"
    Option      "XkbModel" "pc104"
    Option      "XkbLayout" "us,ru"
    Option      "XkbOptions" "grp:alt_shift_toggle"
    Option      "XkbVariant" ",winkeys"
    Option      "AutoRepeat" "250 30"
EndSection

Section "InputDevice"
    Identifier  "Mouse0"
    Driver      "mouse"
    Option      "Protocol" "auto"
    Option      "Device" "/dev/input/mice"
    Option      "ZAxisMapping" "4 5 6 7"
EndSection

Section "Monitor"
    DisplaySize  338 270 # mm
    Identifier   "VGA-1"
    VendorName   "LG"
    ModelName    "Flatron L1730S"
    HorizSync    30.0 - 80.2
    VertRefresh  56.0 - 75.0
    Option       "DPMS"
    # 1280x1024 @ 75.00 Hz (GTF) hsync: 80.17 kHz; pclk: 138.54 MHz
    Modeline "1280x1024_75.00"  138.54  1280 1368 1504 1728  1024 1025 1028 1069  -HSync +Vsync
    # 1024x768 @ 75.00 Hz (GTF) hsync: 60.15 kHz; pclk: 81.80 MHz
    Modeline "1024x768_75.00"  81.80  1024 1080 1192 1360  768 769 772 802  -HSync +Vsync
    # 800x600 @ 75.00 Hz (GTF) hsync: 47.02 kHz; pclk: 48.91 MHz
    Modeline "800x600_75.00"  48.91  800 840 920 1040  600 601 604 627  -HSync +Vsync
    # 640x480 @ 75.00 Hz (GTF) hsync: 37.65 kHz; pclk: 30.72 MHz
    Modeline "640x480_75.00"  30.72  640 664 728 816  480 481 484 502  -HSync +Vsync
    Option       "PreferredMode" "1280x1024_75.00"
EndSection

Section "Monitor"
    DisplaySize    450 350 # mm
    Identifier     "TV-1"
    VendorName     "LG"
    ModelName      "Flatron RT-21FA32X"
    HorizSync      15.625 - 29.7
    VertRefresh    50.0 - 50.0
    Option         "DPMS"
    # 720x576 @ 50.00 Hz (GTF) hsync: 29.65 kHz; pclk: 26.57 MHz
    Modeline "720x576_50.00"  26.57 720 736 808 896  576 577 580 593  -HSync +Vsync
    Option         "PreferredMode" "720x576_50.00"
    Option         "RightOf" "VGA-1"
EndSection

Section "Device"
    Identifier  "Card0"
    Driver      "nouveau"
    VendorName  "nVidia Corporation"
    BoardName   "NV43 [GeForce 6600]"
    BusID       "PCI:5:0:0"

    Option      "Monitor-VGA-1" "VGA-1"
    Option      "Monitor-TV-1" "TV-1"
EndSection

Section "Screen"
    Identifier "Screen0"
    Device     "Card0"
    Monitor    "VGA-1"
    DefaultDepth    24
    SubSection "Display"
        Depth     24
    EndSubSection
EndSection
На что следует обратить внимание?

Во-первых, на опции в секции Device, которые устанавливают соответствие между портами видеокарты и секциями мониторов в файле конфигурации. Порты, по крайней мере на моей видеокарте, имеют названия "VGA-1", "DVI-I-1" и "TV-1" (их названия можно узнать с помощью команды xrandr), поэтому в опциях они будут называться "Monitor-VGA-1", "Monitor-DVI-I-1" и "Monitor-TV-1". Названия же секций мониторов могут быть произвольными - я решил назвать их так же, как и порты.

Во-вторых, в секции, описывающей один из мониторов, нужно добавить опцию, указывающую его расположение относительно первого. В моём случае эта опция выглядит так:
Option         "RightOf" "VGA-1"
В-третьих, в секции Screen, описывающей единственный экран, не должно быть посторонних опций вроде ViewPort, Modes и Virtual. Если они указаны, Xrandr может проявить самостоятельность в выборе настроек видеорежимов, если ему что-то не понравится.

В-четвёртых, предпочитаемый видеорежим монитора можно выбрать с помощью опции PreferredMode. Единственный его недостаток заключается в том, что нельзя ещё дополнительно указать частоту кадровой развёртки. У меня Xrandr упорно выбирал наименьшую из возможных. Для того, чтобы заставить его использовать нужную частоту развёртки, пришлось описать каждый режим вручную в опциях Modeline. Список доступных режимов можно узнать с помощью команды xrandr. Строчки Modeline можно посчитать с помощью команды gtf.

В-пятых, опция Modeline может быть молча проигнорирована, если Xrandr решит, что такой режим не поддерживается монитором. Для того, чтобы убедить Xrandr всё-же установить запрошенный видеорежим, нужно ещё вручную задать допустимые пределы вертикальной и горизонтальной частот синхронизации. Их значения можно получить с помощью команды ddcprobe и перед внесением в файл конфигурации отредактировать так, чтобы они соответствовали Modeline'у.

Например, с помощью gtf я рассчитал Modeline для телевизора:
# 720x576 @ 50.00 Hz (GTF) hsync: 29.65 kHz; pclk: 26.57 MHz
Modeline "720x576_50.00"  26.57 720 736 808 896  576 577 580 593  -HSync +Vsync
Чтобы им можно было воспользоваться, нужно чтобы верхнее значение горизонтальной частоты синхронизации равнялось, а лучше - было чуть больше, чем указанное в Modeline (hsync: 29.65 kHz). Исходя из этого, я задрал верхнюю планку в настройке HorizSync:
HorizSync      15.625 - 29.7
Наконец, после прописывания этих настроек, Xrandr установил нужные мне видеорежимы. Нетребовательные люди могут остановиться на устраивающих их настройках, потому что Xrandr довольно хорошо (но не идеально) выбирает режимы сам.

Ещё один подводный камень возник с XFCE. У него есть собственное меню для настройки видеорежимов - "Дисплей" в "Диспетчере настроек". Чтобы настройки XFCE не сбивали то, что прописано в настройках X-сервера, нужно удалить из домашнего каталога файл ~/.config/xfce4/xfconf/xfce4-perchannel-xml/display.xml и больше не заходить в это меню, потому что при запуске этот файл создаётся снова.

И, наконец, завершающий штрих.

Настройка видеорежима в консоли

Драйвер nouveau располагает двумя мониторами с разными разрешениями. В режиме отображения консоли он показывает на обоих мониторах одно и то же изображение. Чтобы изображение целиком отображалось на обоих мониторах, он формирует картинку, размеры которой позволяют отобразить её целиком на мониторе с меньшим разрешением (на телевизоре). Соответственно, на мониторе с большим разрешением используется только прямоугольная область в верхнем левом углу, совпадающая по пиксельным размерам с разрешением второго монитора (телевизора).

Выглядит это не очень приятно, поэтому я настроил на обоих мониторах одинаковое разрешение, которое они оба могут отобразить: 640x480. При этом разрешении текст умещается в экран телевизора, и занимает 100% площади экрана как на мониторе, так и на телевизоре.

Добавим модуль в загрузочный образ системы. Для этого в файле /etc/initramfs-tools/modules добавим строчку с текстом "nouveau" и обновим загрузочный образ:
# update-initramfs -u -k all
Теперь добавим настройки видеорежимов к параметрам ядер Linux, загружаемых с помощью GRUB 2. Для этого откроем файл /etc/defaults/grub и добавим в опцию GRUB_CMDLINE_LINUX необходимые настройки. У меня в этой опции было пусто, поэтому она приняла следующий вид:
GRUB_CMDLINE_LINUX="video=VGA-1:640x480 video=TV-1:640x480"
Теперь сгенерируем новую конфигурацию GRUB 2:
# update-grub
И перезагрузим компьютер. Оба экрана будут использовать в режиме текстовой консоли разрешения 640x480, а в графическом режиме - настройки X-сервера.

Ссылки:
1. Переход на драйвер nouveau с проприетарных драйверов nvidia
2. Enabling Randr12 support in nouveau
3. Kernel Mode-setting
4. http://cgit.freedesktop.org/nouveau/linux-2.6/tree/Documentation/fb/modedb.txt
5. Проблема со шрифтами в консоли...
6. Оптимизация initramfs в Debian
7. Отключение режима обнаружения TV драйвером NOUVEAU
8. Gentoo: переходим на Grub2
9. Не работает OpenGL в Debian(Nouveau)

воскресенье, 6 января 2013 г.

Генераторы и сопрограммы Python

В некоторых программах, которые мне доводилось писать, при работе с базами данных мне бывало нужно оперировать поочерёдно несколькими запросами.

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

Недоумевающим знатокам MySQL и прочих баз данных я могу сказать, что я осведомлён о существовании таких запросов как INSERT IGNORE ..., INSERT INTO ... ON DUPLICATE KEY UPDATE ..., REPLACE INTO ... и INSERT ... SELECT ..., знаю и о том, что можно написать хранимую процедуру или триггер. Всё это может существенно облегчить написание подобных программ, если речь идёт об обработке данных, имеющихся в самой базе данных. Однако данные могут быть не только в базе данных, они могут располагаться в файле, могут быть получены в результате опроса оборудования (ICMP или SNMP) или запроса к веб-сайту. В результате, часто приходится писать заново довольно-таки типовой код, реализующий всю это обработку данных.

В такой ситуации может помочь объектно-ориентированный подход, однако он обладает некоей громоздкостью, которая часто может свести на нет все его преимущества. Более того, такой код, как правило, уже является объектно-ориентированным - в коде фигурируют различные переменные, содержащие подключения к базам данных, курсоры с уже заготовленными запросами, объекты для опроса оборудования и т.п. Когда я подумывал о рефакторинге своих программ, меня останавливал объём необходимых усилий для реализации необходимых объектов, несравнимый с мизерной выгодой, получаемой от их использования.

И вот, в языке Python я обнаружил замечательный способ для решения подобных мелких типовых задач. Это генераторы и сопрограммы.

Генератор представляет собой подпрограмму, которая может вернуть в основную программу значение, но при этом не завершиться, а приостановиться. Когда основной программе потребуется очередное значение, она передаст управление генератору и он продолжит свою работу до тех пор, пока снова не сгенерирует новое значение, или не выбросит исключение, сообщающее о том, что значения закончились.

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

И то и другое можно реализовать с помощью объектов, но при проектировании и реализации объекта придётся в явном виде выделить состояние объекта и его методы, а также реализовать логику, которая будет учитывать текущее состояние объекта для определения способа поведения каждого из методов. Также, зачастую, перед использованием объекта, его бывает нужно настроить, а после использования - правильно удалить. Конечно, объекты более универсальны, однако для решения простых задач они могут показаться избыточными и неуклюжими.

Итак, чтобы не быть голословным, приведу простой пример, копирующий записи из одной таблицы базы данных в другую. Такую задачу можно решить одним запросом INSERT ... SELECT ..., но вспомните всё то, что я написал выше, и учтите, что это лишь пример.
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

def reader(db):
    """Генератор. Читает строки таблицы user"""
    select = db.cursor()
    select.execute('SELECT surname, name, patronym FROM user')
    for row in select:
        yield row
    select.close()

def writer(db):
    """Сопрограмма. Пишет строки в таблицу user2"""
    insert = db.cursor()
    try:
        while True:
            row = (yield)
            try:
                insert.execute('INSERT INTO user2(surname, name, patronym) VALUES(%s, %s, %s)', row)
                db.commit()
            except:
                db.rollback()
    except GeneratorExit:
        insert.close()

def copy(db):
    """
    Подпрогрмма, использующая генератор и сопрограмму для копирования
    содержимого таблицы user в таблицу user2
    """
    write = writer(db)
    write.next()

    for row in reader(db):
        write.send(row)

    write.close()

db = MySQLdb.connect(user = 'user',
                     passwd = 'p4ssw0rd',
                     db = 'database',
                     charset = 'UTF8')

copy(db)

db.close()
Преимущество такого подхода заключается в том, что генераторы и сопрограммы можно использовать многократно, с очень низкими накладными расходами, а код, использующий их, становится более компактным и лёгким для чтения.

Генераторы легко писать и использовать. Тем, кто знаком с генератором xrange, не составит труда понять, как работает генератор reader из примера.

С сопрограммами всё немного посложнее. Во-первых, при использовании сопрограмма выглядит как настоящий объект - нужно создать экземпляр сопрограммы, подготовить его к использованию, а после использования - закрыть. Во-вторых, внутри сопрограммы нужно обрабатывать исключение GeneratorExit, чтобы определить тот момент, когда программа захочет сообщить, что она больше не будет посылать данные в сопрограмму.

В python есть интересный оператор with, который, как мне показалось, хорошо подошёл бы в этой ситуации для того, чтобы упростить использование сопрограмм. К сожалению, сопрограммы не реализуют интерфейс, необходимый для их использования в этом операторе. Однако, ничто не мешает нам реализовать обёртку, которая позволит использовать сопрограммы вместе с оператором with.

Класс-обёртка, который можно использоваться для произвольных сопрограмм, не только для writer из примера:
class wrapper():
    def __init__(self, coro, *args, **kwargs):
        self.coro = coro(*args, **kwargs)

    def __enter__(self):
        self.coro.next()
        return self.coro

    def __exit__(self, type, value, traceback):
        self.coro.close()
        if value is None:
            return True
        return False

    def send(self, *args, **kwargs):
        return self.coro.send(*args, **kwargs)
Переработанная функция копирования, использующая класс-обёртку и оператор with:
def copy(db):
    """
    Подпрогрмма, использующая генератор и сопрограмму для копирования
    содержимого таблицы user в таблицу user2
    """
    with wrapper(writer, db) as write:
        for row in reader(db):
            write.send(row)
Вот такой вариант мне нравится гораздо больше - все подготовительные и "уборочные" операции выполняются незаметно для пользователя. Может быть этот код сыроват, я ещё не волшеб опытный программист на python'е, я только учусь, однако он работает. Если вы знаете, как сделать лучше - милости прошу в комментарии.

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

Бизли предостерегает от комбинирования сопрограмм и генераторов и приводит три типовых случая, когда оператор yield можно использовать без особых опасений: генераторы, потребители и кооперативная многозадачность.

Ссылки:
1. Дэвид Бизли. Python. Подробный справочник, 4-е издание, стр. 40-42, 126-128.
2. Страница на сайте Дэвида Бизли, посвящённая сопрограммам
3. Презентация Дэвида Бизли о сопрограммах