воскресенье, 16 июля 2017 г.

Бэкенд для LDAP-аутентификации в Django

Написал небольшой модуль для аутентификации в Django через LDAP-сервер. Для входа нужно ввести полное имя пользователя в формате user@domain.tld и пароль пользователя.

Модуль учитывает состояние пользователя - заблокирован он в каталоге LDAP или нет. Если пользователь найден в LDAP, но отсутствует в таблице auth_user, то пользователь создаётся и помечается активным. Если пользователь не найден в LDAP (может быть не найден из-за неверного пароля или из-за того, что пользователь заблокирован), но найден в таблице auth_user, то отметка активности пользователя в таблице снимается.

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

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

Модуль аутентификации LDAP может использоваться и совместно со стандартным модулем аутентификации Django. Именно поэтому в базу данных каждый раз вносится случайно сгенерированный пароль. Если бы в базу данных каждый раз вносился один и тот же пароль (пусть даже и в хэшированном виде), то всё ещё оставалась бы возможность войти под учётной записью этого пользователя, зная этот стандартный пароль, а не настоящий пароль пользователя в каталоге LDAP.

Сохраним бэкенд в файл application/ldap_backend.py внутри каталога проекта:
# -*- coding: UTF-8 -*-

import random, ldap
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password

# Функция, генерирующая пароль или ключ указанной сложности
def genkey(upper=True, lower=True, digits=True, signs=False, length=64):
    chars = ''

    if upper:
        chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    if lower:
        chars += 'abcdefghijklmnopqrstuvwxyz'
    if digits:
        chars += '0123456789'
    if signs:
        chars += '()[]{}<>|/~!@#$%^&,.;:*-+=_'

    key = ''
    num = len(chars)
    for i in xrange(0, length):
        j = int(random.random() * num)
        key += chars[j]
    return key

class LDAPBackend(object):
    def authenticate(self, username=None, password=None):
        # Ищем пользователя в LDAP
        try:
            login, domain = username.split('@')
        except ValueError:
            return None

        base_dn = domain.split('.')
        base_dn = map(lambda x: 'dc=' + x, base_dn)
        base_dn = ','.join(base_dn)

        l = ldap.initialize('ldap://' + domain)
        try:
            l.simple_bind_s(username, password)
        except ldap.INVALID_CREDENTIALS:
            return None
        l.set_option(ldap.OPT_REFERRALS, 0)
        result = l.search_s(base_dn,
                            ldap.SCOPE_SUBTREE,
                            '(&(sAMAccountName=%s)(objectClass=user)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))' % login,
                            None)
        found, data = result[0]
        l.unbind_s()

        # Ищем пользователя в django или создаём его
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            user = User(username=username)
            user.password = make_password(genkey())
            user.is_staff = True
            user.is_superuser = False

        # Обновляем пользователя в django
        user.email = data.get('mail', [''])[0]
        user.first_name = data.get('givenName', [''])[0].decode('UTF-8') # data.get('middleName', [''])[0].decode('UTF-8')
        user.last_name = data.get('sn', [''])[0].decode('UTF-8')
        if found:
            user.is_active = True
        else:
            user.is_active = False
        user.save()

        return user

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None
Для использования этого модуля аутентификации, нужно прописать его в настройках проекта, в файле settings.py:
AUTHENTICATION_BACKENDS = (
  'application.ldap_backend.LDAPBackend',
  'django.contrib.auth.backends.ModelBackend'
)
Если стандартная аутентификация Django не требуется, то стандартный модуль можно отключить.

Если у кого-то есть замечания или предложения по доработке модуля - готов вас выслушать.

воскресенье, 9 июля 2017 г.

Обработка аутентификации в Django

Частичный перевод статьи: Handling Authentication & Authorization
Автор: Натан Йерглер (Nathan Yergler)

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

Чтобы воспользоваться встроенной поддержкой аутентификации, к проекту нужно подключить приложения django.contrib.auth и django.contrib.sessions.

Как можно увидеть в addressbook/settings.py, при создании проекта Django, они уже включены по умолчанию.
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Раскомментируйте следующую строку, чтобы включить интерфейс администрирования:
    # 'django.contrib.admin',
    # Раскомментируйте следующую строку, чтобы включить документацию на интерфейс администрирования:
    # 'django.contrib.admindocs',
    'contacts',
)
Кроме установки приложений, нужно также установить промежуточное программное обеспечение.
MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    # Раскомментируйте следующую строку для включения простой защиты от перехвата щелчков мыши:
    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
Как мы помним, во время первого запуска syncdb Django задаёт вопрос - нужно ли создать учётную запись суперпользователя. Это происходит, потому что промежуточное программное обеспечение уже установлено.

В модуле auth из стандартной поставки Django имеются модели User - пользователь, Group - группа и Permission - права. Обычно этого бывает достаточно, если не нужно интегрировать приложение с другой системой аутентификации.

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

Для рассматриваемого примера нужно просто добавить в наш проект представления login и logout. Для начала добавим представления в
файл addressbook/urls.py.
urlpatterns = patterns('',
    url(r'^login/$', 'django.contrib.auth.views.login'),
    url(r'^logout/$', 'django.contrib.auth.views.logout'),
У обоих представлений login и logout есть имена шаблонов по умолчанию (registration/login.html и registration/logged_out.html, соответственно). Поскольку эти представления специфичны для нашего проекта и не используются повторно приложением Contacts, мы создадим новый каталог templates/registration внутри каталога приложения addressbook:
$ mkdir -p addressbook/templates/registration
И сообщим Django о необходимости искать шаблоны в этом каталоге, настроив TEMPLATE_DIRS в addressbook/settings.py.
TEMPLATE_DIRS = (
    # Поместите здесь строки, такие как "/home/html/django_templates" или "C:/www/django/templates".
    # Всегда используйте прямую косую черту, даже в Windows.
    # Не забывайте, что нужно указывать полный путь, а не относительный.
    'addressbook/templates',
)
Внутри этого каталога сначала создадим файл login.html.
{% extends "base.html" %}

{% block content %}

{% if form.errors %}
<p>Ваше имя пользователя и пароль не подходят. Попробуйте ещё раз.</p>
{% endif %}

<form method="post" action="{% url 'django.contrib.auth.views.login' %}">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>

<input type="submit" value="Войти" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
{% endblock %}
Шаблон login наследуется от шаблона base.html и отображает форму входа, переданную в шаблон из представления. Скрытое поле next позволяет представлению впоследствии перенаправить пользователя на запрошенную страницу, если при запросе этой страницы пользователь был перенаправлен на страницу входа.

Шаблон выхода - logged_out.html - значительно проще.
{% extends "base.html" %}

{% block content %}

Вы вышли!

{% endblock %}
Всё, что нужно - только сообщить пользователю, что выход завершился успешно.

Если сейчас запустить сервер разработки с помощью runserver и перейти на страницу по ссылке http://localhost:8000/login, то можно увидеть страницу входа. При попытке входа с неправильными данными можно увидеть сообщение об ошибке. Теперь давайте попробуем войти с данными суперпользователя, который был создан ранее.

Погодите, что случилось? Почему мы попали на страницу /accounts/profile? Мы не вводили такой адрес. После успешного входа представление login перенаправляет пользователя на определённый URL и по умолчанию это страница /accounts/profile. Чтобы заменить её, нужно настроить значение LOGIN_REDIRECT_URL в addressbook/settings.py так, чтобы при первом входе пользователь перенаправлялся бы на страницу со списком контактов.
LOGIN_REDIRECT_URL = '/'
Теперь, когда мы можем войти и выйти, было бы неплохо показывать в заголовке имя вошедшего пользователя и ссылки для входа/выхода. Добавим их в наш шаблон base.html, т.к. нам нужно отображать их везде.
<body>
    <div>
    {{ user }}
    {% if user.is_anonymous %}
    <a href="{% url 'django.contrib.auth.views.login' %}">Войти</a>
    {% else %}
    <a href="{% url 'django.contrib.auth.views.logout' %}">Выйти</a>
    {% endif %}
    </div>

воскресенье, 2 июля 2017 г.

Краткий учебник по Django

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

О терминологии веб-фреймворка Django

  • Проект - совокупность приложений, имеющих общие настройки.
  • Приложение - часть проекта, выполняющая определённую логически неделимую функцию. Состоит из представлений (views), шаблонов (templates) и моделей (models).
  • Шаблон - шаблон HTML-страницы. В терминологии MVC шаблону соответствует представление.
  • Модель - средство доступа к данным.
  • Представление - связующий код между моделью и шаблоном. В терминологии MVC представлению соответствует контроллеру.
  • Маршрут - соответствие между URL'ом и представлением (контроллером в терминологии MVC), отвечающим за этот URL.
Программы, написанные с использованием Django, являются совокупностью отдельных приложений, объединённых в один проект. В отличие от многих других веб-фреймворков, в Django не используется архитектура MVC - Модель-Представление-Контроллер, вместо неё используется собственная архитектура MVT - Модель-Представление-Шаблон. Шаблон Django по функциям ближе всего к представлению в MVC, а представление Django по функциям ближе всего к контроллеру в MVC. Представления Django привязываются к определённому URL через маршруты.

Установка Python и Django

В Debian и производных от него дистрибутивах установить Python и Django можно из репозиториев:
# apt-get install python python-django

Создание проекта и приложения

Создаём проект dj:
$ django-admin startproject dj
Переходим в каталог проекта:
$ cd dj
Смотрим содержимое каталога проекта:
$ find .
.
./manage.py
./dj
./dj/wsgi.py
./dj/settings.py
./dj/__init__.py
./dj/urls.py
Создаём в проекте новое приложение app:
$ ./manage.py startapp app
В каталоге проекта появится новый каталог с именем app, созданный специально для размещения приложения:
$ find .
.
./manage.py
./dj
./dj/wsgi.py
./dj/settings.py
./dj/__init__.py
./dj/urls.py
./dj/settings.pyc
./dj/__init__.pyc
./app
./app/tests.py
./app/views.py
./app/models.py
./app/__init__.py

Создание представления

Создадим новое представление hello в приложении app. Представление принимает в качестве аргумента объект HttpRequest и возвращает объект HttpResponse.

Откроем для редактирования файл app/views.py и придадим ему следующий вид:
# -*- coding: UTF-8 -*-
from django.http import HttpResponse

def hello(request):
    return HttpResponse(u'Здравствуй, мир!')

Создание маршрута

Теперь настроим маршрут, вызывающий это представление для url. Для этого откроем файл dj/urls.py и добавим в него пару строчек.

Первую строчку добавим в начало файла, после других строчек импорта. Строчка импортирует представление hello из приложения app:
from app.views import hello
Теперь найдём функцию patterns, возвращаемое значение которой присваивается переменной urlpatterns, и впишем в аргументы функции следующую строчку:
('^hello/$', hello),
В итоге у меня содержимое файла dj/urls.py приняло следующий вид:
from django.conf.urls import patterns, include, url
from app.views import hello

# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'dj.views.home', name='home'),
    # url(r'^dj/', include('dj.foo.urls')),

    # Uncomment the admin/doc line below to enable admin documentation:
    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    # url(r'^admin/', include(admin.site.urls)),
    ('^hello/$', hello),
)
В шаблоне url в конце обязательно должна быть косая черта, т.к. если клиент запросит страницу без косой черты в конце, Django автоматически добавит её и попытается найти представление, соответствующее этому URL. Нужно ли добавлять косую черту, регулируется настройкой APPEND_SLASH. Если установить её в False, то косая черта добавляться не будет. Шаблоном для корня сайта является '^$', то есть соответствие пустой строке.

Включение приложения в проекте

Чтобы в проекте использовалось наше приложение, его нужно подключить к проекту. Для этого закомментируем на время список стандартных приложений в файле dj/settings.py и пропишем наше приложение:
INSTALLED_APPS = (
    #'django.contrib.auth',
    #'django.contrib.contenttypes',
    #'django.contrib.sessions',
    #'django.contrib.sites',
    #'django.contrib.messages',
    #'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'app',
)
Также закомментируем в настройках подключение приложений-прослоек:
MIDDLEWARE_CLASSES = (
    #'django.middleware.common.CommonMiddleware',
    #'django.contrib.sessions.middleware.SessionMiddleware',
    #'django.middleware.csrf.CsrfViewMiddleware',
    #'django.contrib.auth.middleware.AuthenticationMiddleware',
    #'django.contrib.messages.middleware.MessageMiddleware',
    # Uncomment the next line for simple clickjacking protection:
    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

Запуск сервера разработчика

Теперь настало время запустить сервер разработчика с только что созданным простейшим приложением:
$ ./manage.py runserver 0.0.0.0:8000
Можно попытаться зайти в браузере на страницу /hello/. У меня ссылка для открытия страницы выглядела так:
http://localhost:8000/hello/
Итак, мы создали наш первый проект на Django, который при обращении к странице /hello/ выводит надпись "Здравствуй, мир!" Возможности Django в этом проекте практически не используются - мы не использовали ни моделей, ни шаблонов, но этот проект даёт общее представление о структуре программ, написанных с использованием Django.

Использование шаблонов

Для начала настроим каталог, в котором будут находиться шаблоны. Для этого отредактируем файл dj/settings.py и впишем в кортеж TEMPLATES_DIRS полный путь к каталогу с шаблонами.

После редактирования файла эта настройка в файле settings.py приняла следующий вид:
TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
    "/home/stupin/dj/templates",
)
Не забывайте в конце одноэлементного кортежа поставить запятую, чтобы Python мог отличить кортеж от простого выражения в скобках.

Теперь создадим каталог для шаблонов:
$ mkdir templates
И создадим в нём новый шаблон с именем time.tmpl и со следующим содержимым:
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Текущее время</title>
  </head>
  <body>
    <div align="center">
      Сейчас {{ time }}
    </div>
  </body>
</html>
Теперь добавим в файл app/views.py импорт функции для загрузки и отрисовки шаблона:
from django.shortcuts import render_to_response
И создадим представление current_datetime следующим образом:
def current_datetime(request):
    return render_to_response('time.tmpl', {'time' : datetime.now()})
Осталось настроить в файле urls.py маршрут к этому представлению:
('^time/$', current_datetime),
Если сейчас попробовать открыть страницу http://localhost:800/time/, то можно увидеть текущее время на английском языке в часовом поясе Гринвичской обсерватории.

Настройка часового пояса и языка

Чтобы время отображалось правильно - в часовом поясе сервера, пропишем в файл настроек местный часовой пояс. Для этого откроем файл dj/settings.py и пропишем в переменную TIME_ZONE значение 'Asia/Yekaterinburg':
TIME_ZONE = 'Asia/Yekaterinburg'
Чтобы время на странице отображалось в соответствии с правилами, принятыми в России, пропишем в файл настроек язык проекта. Откроем файл dj/settings.py и пропишем в переменную LANGUAGE_CODE значение 'ru-RU':
LANGUAGE_CODE = 'ru-RU'
Теперь текущее время должно отображаться на русском языке в часовом поясе, по которому живёт Уфа :)

Пример более сложного маршрута

Попробуем добавить страницы, которые будут вычитать из текущего времени часы, фигурирующие в URL запрошенной страницы. Для этого в файл dj/urls.py добавим маршруты:
(r'^time/plus/(\d{1,2})$', hours_plus),
(r'^time/minus/(\d{1,2})$', hours_minus),
В каталоге с шаблонами templates разместим два новых шаблона.

Файл templates/time_minus.tmpl со следующим содержимым:
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Прошлое время</title>
  </head>
  <body>
    <div align="center">
      {{ delta }} часов назад было {{ time }}
    </div>
  </body>
</html>
Файл templates/time_plus.tmpl со следующим содержимым:
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Будущее время</title>
  </head>
  <body>
    <div align="center">
      Через {{ delta }} часов будет {{ time }}
    </div>
  </body>
</html>
В файл app/views.py пропишем два представления, которые будут использовать два новых шаблона:
def hours_plus(request, delta):
    try:
      delta = int(delta)
    except ValueError:
      raise Http404()
    time = datetime.now() + timedelta(hours=delta)
    return render_to_response('time_plus.tmpl', {'delta' : delta,
                                                 'time' : time})

def hours_minus(request, delta):
    try:
      delta = int(delta)
    except ValueError:
      raise Http404()
    time = datetime.now() - timedelta(hours=delta)
    return render_to_response('time_minus.tmpl', {'delta' : delta,
                                                  'time' : time})
Файл app/views.py целиком примет следующий вид:
# -*- coding: UTF-8 -*-
from django.http import HttpResponse, Http404
from django.shortcuts import render_to_response
from datetime import datetime, timedelta

def hello(request):
    return HttpResponse(u'Здравствуй, мир!')

def current_datetime(request):
    return render_to_response('time.tmpl', {'time' : datetime.now()})

def hours_plus(request, delta):
    try:
      delta = int(delta)
    except ValueError:
      raise Http404()
    time = datetime.now() + timedelta(hours=delta)
    return render_to_response('time_plus.tmpl', {'delta' : delta,
                                                 'time' : time})

def hours_minus(request, delta):
    try:
      delta = int(delta)
    except ValueError:
      raise Http404()
    time = datetime.now() - timedelta(hours=delta)
    return render_to_response('time_minus.tmpl', {'delta' : delta,
                                                  'time' : time})
Теперь можно перейти по ссылкам http://localhost:8000/time/plus/1 или http://localhost:8000/time/plus/2 и увидеть получающиеся страницы.

Более сложные шаблоны

Условие:
{% if today_is_weekend %}
    <p>Сегодня выходной!</p>
{% endif %}
Условие с двумя вариантами:
{% if today_is_weekend %}
    <p>Сегодня выходной!</p>
{% else %}
    <p>Пора работать.</p>
{% endif %}
Ложным значениями являются: пустой список [], пустой кортеж (), пустой словарь {}, ноль - 0, объект None и объект False.

Можно использовать сочетания условий при помощи and и or, причём and имеет более высокий приоритет. Скобки в условиях не поддерживаются, без них можно обойтись с помощью вложенных условий. Также возможно использовать операторы ==, !=, <, >, >=, <= и in для вычисления условий, по смыслу совпадающих с условными операторами в самом Python. Циклы для перебора значений из списка:
{% for athlete in athlete_list %}
  <li>{{ athlete.name }}</li>
{% endfor %}
Можно перебирать значения из списка в обратном порядке:
{% for athlete in athlete_list reversed %}
...
{% endfor %}
Внутри циклов существуют следующие переменные:
  • {{ forloop.counter }} - номер итерации, начиная с 1,
  • {{ forloop.counter0 }} - номер итерации, начиная с 0,
  • {{ forloop.revcounter }} - количество оставшихся итераций, включая текущую. Внутри первой итерации равно количеству элементов, на последней итерации - 1,
  • {{ forloop.revcounter }} - количество оставшихся итераций. Внутри первой итерации содержит количество элементов за минус единицей, на последней итерации - 0,
  • {% if forloop.first %} - условие выполняется на первой итерации,
  • {% if forloop.last %} - условие выполняется на последней итерации,
  • forloop.parentloop - ссылка на объект родительского цикла.
Комментарии в шаблоне:
{# Это комментарий #}
или
{% comment %}
Многострочный
комментарий.
{% endcomment %}

Включение подшаблонов

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

Включение подшаблона из файла:
{% include "includes/nav.html" %}
Включение подшаблона из переменной:
{% include template_name %}

Наследование шаблонов

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

Продолжим эксперименты с нашим тестовым проектом, добавив в него базовый шаблон.

Базовый шаблон base.tmpl:
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>{% block title %}{% endblock %}</title>
  </head>
  <body>
    <h1>Бесполезный сайт с часами</h1>

    {% block content %}{% endblock %}

    {% block footer %}
    <hr>
    <p>Благодарим за посещение нашего сайта.</p>
    {% endblock %}
  </body>
</html>
В базовом шаблоне помечаются блоки, в которых прописывается часть шаблона, специфичная для этой страницы.

Производный шаблон. Для примера приведу только содержимое шаблона time_plus.tmpl:
{% extends "base.tmpl" %}

{% block title %}Будущее время{% endblock %}

{% block content %}
<p>Через {{ delta }} часов будет {{ time }}</p>
{% endblock %}
В производном шаблоне указывается базовый шаблон и переопределяется содержимое блоков базового шаблона. Шаблоны других страниц можно отредактировать сходным образом, чтобы и они использовали дизайн, общий для всех страниц.

Настройка базы данных MySQL

После того, как мы рассмотрели шаблоны Django, пришло время заняться моделями Django. Но прежде чем приступить непосредственно к изучению моделей, нужно установить необходимые модули, выставить настройки как в самой СУБД, так и в проекте Django.

Установим модуль для доступа к базе данных MySQL из Python (разработчики фреймворка Django рекомендуют использовать PostgreSQL, но мы воспользуемся MySQL, поддержка которого тоже имеется):
# apt-get install python-mysqldb
Настроим кодировку сервера MySQL и порядок сортировки. Для этого в файле /etc/mysql/my.cnf в секцию [mysqld] впишем следующие настройки:
character_set_server=utf8
collation_server=utf8_unicode_ci
Перезапустим сервер базы данных:
# /etc/init.d/mysql restart
В результате вышеописанных действий должен получиться такой результат:
mysql> show variables like 'coll%';
+----------------------+-----------------+
| Variable_name        | Value           |
+----------------------+-----------------+
| collation_connection | utf8_general_ci |
| collation_database   | utf8_unicode_ci |
| collation_server     | utf8_unicode_ci |
+----------------------+-----------------+
3 rows in set (0.00 sec)

mysql> show variables like 'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)
Создадим базу данных для проекта:
CREATE USER app@localhost IDENTIFIED BY "app_password";
CREATE DATABASE app;
GRANT ALL ON app.* TO app@localhost;
Пропишем в файл настроек проекта dj/settings.py настройки подключения к базе данных:
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
        'NAME': 'app',                        # Or path to database file if using sqlite3.
        'USER': 'app',                        # Not used with sqlite3.
        'PASSWORD': 'app_password',           # Not used with sqlite3.
        'HOST': '',                           # Set to empty string for localhost. Not used with sqlite3.
        'PORT': '',                           # Set to empty string for default. Not used with sqlite3.
    }
}
Проверяем правильность настройки:
$ ./manage.py shell
Если всё правильно, то откроется отладочная оболочка Python. Попробуем ввести следующие команды:
from django.db import connection
cursor = connection.cursor()
Если сообщений об ошибках не было, значит всё настроено правильно.

Создание моделей

Каждая модель Django описывают одну таблицу в базе данных. В Django имеется система отображения таблиц в объекты - ORM - Object-Relational Mapping - объектно-реляционное отображение. ORM позволяет манипулировать единичными строками таблиц как объектами Python, а также осуществлять операции массовой выборки, обновления и удаления строк.

В качестве примера, рассмотрим адресный справочник домов в городах. Откроем файл app/models.py и приведём его к следующему виду:
# -*- coding: UTF-8 -*-
from django.db import models

class City(models.Model):
    country = models.ForeignKey(Country)
    title = models.CharField(max_length=150)

class Street(models.Model):
    city = models.ForeignKey(City)
    title = models.CharField(max_length=150)

class Area(models.Model):
    city = models.ForeignKey(City)
    title = models.CharField(max_length=150)

class House(models.Model):
    area = models.ForeignKey(Area)
    street = models.ForeignKey(Street)
    house = models.IntegerField()
    frac = models.CharField(max_length=30)
    comment = models.CharField(max_length=100)
Мы описали объекты, составляющие адресный справочник, и описали взаимоотношения между ними через внешние ключи - ForeignKey. В городе имеются улицы и районы, а каждый дом находится на одной улице и принадлежит одному из районов города.

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

Теперь мы можем проверить правильность синтаксиса и логики моделей:
$ ./manage.py validate
Чтобы увидеть команды SQL для создания структуры базы данных, требуемой для моделей из приложения app, введём следующую команду:
$ ./manage.py sqlall app
Чтобы выполнить эти операторы SQL и создать в базе данных таблицы, соответствующие моделям, нужно выполнить следующую команду:
$ ./manage.py syncdb
Можно войти в базу данных клиентом и увидеть созданную структуру таблиц и их взаимосвязи.

Для входа в базу данных с настройками проекта, можно воспользоваться следующей командой:
$ ./manage.py dbshell

Создание записей в таблицах

Откроем оболочку Python:
$ ./manage.py shell
Импортируем описание моделей:
from address.models import *
Создадим объект "город":
c = City(title=u'Уфа')
И сохраним его в базу данных:
c.save()
Теперь создадим объект "улица" в этом городе:
s = Street(title=u'ул. Карла Маркса', city=c)
И сохраним объект с улицей:
s.save()
Если нужно отредактировать объект, то можно прописать в него новое свойство и сохранить:
c.title = u'г. Уфа'
c.save()
Недостаток такого рода редактирования объектов заключается в том, что в базе данных обновляются все поля в отредактированной строке таблицы, а не только то поле, которое действительно было изменено. Для раздельного редактирования полей строк можно воспользоваться массовым редактированием, о котором будет рассказано далее.

Извлечение записей из таблиц

Откроем оболочку Python:
$ ./manage.py shell
Импортируем описание модели:
from address.models import *
Загрузим все объекты типа "город":
c = City.objects.all()
Загрузим объект "город", имеющий имя "г. Уфа":
c = City.objects.filter(title=u'г. Уфа')
И убедимся, что загрузился именно он:
print c
Выбор списка объектов:
  • Obj.objects.all() - отобрать как список все объекты типа Obj,
  • Obj.objects.filter(field='') - отобрать как список объекты, у которых поле field имеет указанное значение,
  • Obj.objects.filter(field1='', field2='') - отобрать как список объекты, у которых оба поля одновременно имеют указанные значения,
  • Obj.objects.filter(field__contains='') - отобрать как список объекты, у которых в указанном поле содержится указанное значение,
  • Obj.objects.filter(field__icontains='') - отобрать как список объекты, у которых в указанном поле содержится указанное значение без учёта регистра,
  • Obj.objects.filter(field__iexact='') - отобрать как список объекты, у которых указанное поле совпадает с указанным значением без учёта регистра,
  • Obj.objects.filter(field__startswith='') - отобрать как список объекты, у которых указанное поле начинается с указанного значения,
  • Obj.objects.filter(field__istartswith='') - отобрать как список объекты, у которых указанное поле начинается с указанного значения без учёта регистра,
  • Obj.objects.filter(field__endswith='') - отобрать как список объекты, у которых указанное поле заканчивается указанным значением,
  • Obj.objects.filter(field__iendswith='') - отобрать как список объекты, у которых указанное поле заканчивается указанным значением без учёта регистра.
Условия в фильтре можно комбинировать между собой не только указывая их через запятую, но создавая каскады фильтров:
Obj.objects.filter(field1__iendswith='a').filter(field2='b')
Чтобы отобрать объекты, не подходящие под указанное условие, можно воспользоваться методом фильтрации exclude. Например, следующее выражение отберёт те записи, у которых начало первого поля без учёта регистра совпадает с a, а второе поле не равно b:
Obj.objects.filter(field1__iendswith='a').exclude(field2='')
Выбор одного объекта осуществляется точно таким же способом, как выбор списка, за исключением того, что вместо метода filter используется метод get:
Obj.objects.get(field='')
Если ни один объект не был найден, будет сгенерировано исключение Obj.DoesNotExist. Если указанным критериям соответствует несколько записей, то будет сгенерировано исключение Obj.MultipleObjectsReturned.

Как и в случае выборки списка объектов, можно комбинировать фильтры друг с другом. Но для выборки одного объекта последним методом в цепочке должен быть get. Например, вот так:
Obj.objects.filter(field1__iendswith='a').exclude(field2='').get()
Или, что то же самое, вот так:
Obj.objects.exclude(field2='').get(field1__iendswith='a')
В Django версий 1.6 и более поздних имеется метод first(), не принимающий аргументов, который возвращает первую запись из списка, если она есть. В противном случае возвращается None. Стоит учитывать, что этот метод никак не обрабатывает случаи, когда условию соответствует несколько записей сразу.

Сортировка данных:
  • Obj.objects.order_by("field") - сортировка по одному полю,
  • Obj.objects.order_by("-field") - сортировка по одному полю в обратном порядке,
  • Obj.objects.order_by("field1", "field2") - сортировка по двум полям.
Сортировку по умолчанию можно настроить в свойствах объекта модели, добавив вложенный класс со свойством ordering, которому присвоен список полей для сортировки:
    class Meta:
        ordering = ["field"]
Можно комбинировать методы:
Obj.objects.filter(field="").order_by("-field")
Можно выбирать необходимый фрагмент списка объектов. Например, вот этот запрос вернёт два первых объекта:
Obj.objects.filter(field="").order_by("-field")[0:2]
Массовое обновление объектов осуществляется следующим образом:
Obj.objects.filter(id=52).update(field='')
Запрос возвращает количество обновлённых строк таблицы.

Удалить один объект можно следующим образом:
o = Obj.objects.get(field='')
o.delete()
Удалить объекты массово можно так:
Obj.objects.filter(field='').delete()

Активация интерфейса администратора

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

Для включения интерфейса администрирования нужно внести изменения в файл настроек dj/settings.py:
  1. Вписать в INSTALLED_APPS приложения django.contrib.admin, django.contrib.auth, django.contrib.sessions и django.contrib.contenttypes,
  2. Вписать в MIDDLEWARE_CLASSES приложения-прослойки django.middleware.common.CommonMiddleware, django.contrib.sessions.middleware.SessionMiddleware, django.contrib.auth.middleware.AuthenticationMiddleware и django.contrib.messages.middleware.MessageMiddleware.
Вносим изменения в базу данных, чтобы в ней создались таблицы, необходимые для работы интерфейса администрирования:
$ ./manage.py syncdb
Если при этом отказаться от создания суперпользователя, то потом его можно создать с помощью команды:
$ ./manage.py createsuperuser
Теперь в начало файла dj/urls.py добавим использование модуля и его инициализацию:
from django.contrib import admin
admin.autodiscover()
И пропишем маршрут к интерфейсу администрирования:
urlpatterns = patterns('',
    # ...
    (r'^admin', include(admin.site.urls)),
    # ...
)
Для того, чтобы объекты можно было редактировать прямо из интерфейса администратора, нужно создать в каталоге приложения app файл admin.py со следующим содержимым:
from app.models import *
from django.contrib import admin

admin.site.register(City)
admin.site.register(Area)
admin.site.register(Street)
admin.site.register(House)
После этого можно перейти по ссылке http://localhost:8000/admin/, войти под учётными данными, указанными команде createsuperuser, и пользоваться интерфейсом администрирования для добавления, редактирования и удаления записей в таблицах.

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

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

воскресенье, 25 июня 2017 г.

Unix для Perl-программистов: каналы и процессы

Перевод: Unix for Perl programmers: pipes and processes
Автор: Аарон Крейн (Aaron Crane)

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

Вспомнил о неоконченном переводе, когда нашёл на CPAN модуль, который оказался аккуратнее моего модуля, а делал в точности то же самое. Это модуль Parallel::DataPipe, написанный Александром Харченко. К сожалению, и на этот раз ситуация повторилась и я опять надолго забыл про перевод.

И вот уже та программа полностью переделана на Python и модуль multiprocessing, а статья всё оставалась не переведённой. Если работа не завершена, значит время было потрачено впустую. Поэтому решил довести дело до конца - перевести остаток статьи, вычитать перевод, согласовать терминологию, оформить и выложить.
Это сопроводительный документ к выступлению на YAPC::EU 2009.

Одна из общеизвестных сильных сторон Perl заключается в том, что он скрывает избыточную сложность, делая простые вещи простыми. В контексте Unix-программирования это означает, что, например, можно легко захватить вывод команды, выполненной в shell:
my $ps_output = `ps`;
Эта строчка запустит команду ps, захватит весь её вывод и сохранит его в переменную $ps_output. Потрясающе просто.

Однако, не всегда можно обойтись стандартными средствами Perl. Например, что если понадобится запустить несколько процессов параллельно? Что если потребуется одновременно канал для ввода в процесс и канал для захвата его вывода?

К счастью, назначение Perl заключается и в том, чтобы сделать сложное возможным: он предоставляет свободный доступ к низкоуровневому API Unix. Поэтому, если вы знаете, что средств Unix достаточно для достижения ваших целей, Perl сможет вам помочь.

Итак, статья вот-вот начнётся. Она покажет всю подноготную каналов, процессов и сигналов с точки зрения Perl-программиста, поэтому если вам когда-нибудь потребуется что-то из названного, вы поймёте, что нужно делать.

CPAN

Одна из классических добродетелей хорошего программиста - это лень. В частности, Perl-программисту важно знать, как (и когда) стоит воспользоваться чужими наработками из CPAN. Не секрет, что на CPAN уже имеются реализации многих описанных здесь приёмов. Но поскольку моя цель - объяснить что происходит за кулисами Unix, я поступлю необычно - просто пропущу этап поиска модулей на CPAN.

Как запустить программу?

Начнём с простого вопроса, ответ на который сформирует основы для понимания работы процессов в Unix: как запустить программу и дождаться её завершения?

Приятно, что в Perl есть простой встроенный способ для ответа на этот вопрос, по крайней мере он просто выглядит. Если нужно запустить программу с именем update_web_server, можно просто воспользоваться именем этой программы, как аргументом для функции system, встроенной в Perl:
system 'update_web_server';
Но тут же возникает следующий вопрос: как узнать, что программа выполнилась успешно? В Unix каждый процесс завершает работу, возвращая статус завершения - целое число, доступное вызвавшему процессу. По устоявшейся традиции, если процесс завершил работу со статусом 0, то это значит, что он успешно выполнил то, что требовалось. Соответственно - ненулевой статус свидетельствует об ошибке.

(Многие люди удивляются, почему ноль означает успех, а не ноль означает ошибку, потому что во многих языках программирования ноль считается ложью, а не ноль - истиной. Причина в том, что у большинства программ существует только один вариант успешного завершения, но множество неуспешных вариантов. И некоторые программы этим пользуются. Например, grep(1) возвращает 0 - если было найдено совпадение, 1 - если совпадение не найдено и 2 - если произошла ошибка, например - ошибка открытия файла.)

Естественно, в Perl имеется простой способ узнать статус завершения запущенной команды: переменная $? содержит статус завершения последней выполненной команды. (Важно, что функция system возвращает это же значение.) Однако, $? - это не просто статус завершения: если процесс завершился из-за того, что операционная система отправила неожиданный сигнал, этот сигнал также кодируется в $?. Подробное описание можно найти в perlvar, а сейчас достаточно знать, что $? равен нулю в случае успешного завершения процесса.

Соглашение о нуле в случае успеха приводит к тому, что им неудобно пользоваться для обнаружения ошибок. Проще всего это сделать так:
system('update_web_server') == 0
    or die "Не удалось обновить веб-серверы\n";
Следующий вопрос заключается в том, как передать программе аргументы. В некоторых случаях лучше воспользоваться наиболее очевидным подходом - просто вставить аргументы в вызов system:
system 'update_web_server --all';
Однако, в более сложных случаях этот подход может не подойти. Когда функция system вызывается с одним строковым аргументом, функция трактует этот аргумент как команду, которую нужно выполнить при помощи /bin/sh - оболочки операционной системы. Но что будет, если часть строки поступит из источника, не заслуживающего доверия?
my $host = $ARGV[0];
system "update_web_server --host=$host";
Всё выглядит довольно очевидно, но что если $ARGV[0] - это x; rm -rf /? Поскольку при указании единственного аргумента функция system вызывает оболочку для запуска программы, оболочка сначала запустит update_web_server с аргументом --host=x, а затем приступит к выполнению rm -rf /. Ой!

К счастью, в Perl есть выход: если передать функции system список аргументов, а не одну строку с командой, функция трактует список как имя программы и её аргументы, и запустит программу напрямую, без вызова оболочки:
system 'update_web_server', "--host=$host";

Как запустить программу в фоновом режиме?

Представим, что нужно запустить программу, работа которой займёт много времени, но при этом нужно продолжить работу основной программы. Можно воспользоваться средствами оболочки - добавить амперсанд к команде, чтобы запустить её в фоновом режиме:
system 'update_web_server --all &';
Однако, как было написано в предыдущем разделе, это небезопасно, если команда должна обработать данные из недоверенного источника. Можно обработать данные, применив приёмы экранирования данных перед их встраиванием в команду. Но существует и другой подход: можно воспользоваться низкоуровневыми средствами Unix - разветвлением процесса при помощи системного вызова fork.

С низкоуровневой точки зрения разветвление - это способ создания нового процесса в Unix. Любопытно, что разветвление не подразумевает запуск программы. Напротив, каждый созданный процесс является практически идентичным клоном - ответвлением от породившего процесса.

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

(Термины "родитель" и "потомок" для создавшего и созданного процессов распространены среди большинства Unix-хакеров и даже встречается в описании некоторых системных вызовов, например - getppid(2). Иногда даже употребляются термины "прародитель", "внук", "брат" в их очевидном смысле.)

Как выглядит разветвление с точки зрения Perl? Во-первых, родитель вызывает встроенную подпрограмму fork, которая является тонкой обёрткой вокруг системного вызова fork(2):
my $pid = fork;
В родителе этот вызов возвращает идентификатор процесса нового потомка (или неопределённое значение, если по некоторой причине вызов fork завершился ошибкой). Однако, в созданном потомке код продолжает работать точно так же, за исключением того, что вызов fork в нём возвращает ноль. Поясним происходящее на примере следующего кода:
my $pid = fork;
die "Не удалось выполнить fork: $!\n" if !defined $pid;
if ($pid == 0) {
    # В потомке
}
else {
    # В родителе. Потомок имеет идентификатор процесса $pid
}
По крайней мере этот пример характерен для программ на C, но для Perl он может оказаться излишним. Остаток этого документа будет опираться на функцию fork_child, определённую на Perl следующим образом:
sub fork_child {
    my ($child_process_code) = @_;

    my $pid = fork;
    die "Не удалось выполнить fork: $!\n" if !defined $pid;

    return $pid if $pid != 0;

    # Теперь мы в потомке
    $child_process_code->();
    exit;
}
Эта функция получает один аргумент - ссылку на некий код, который должен быть выполнен в потомке. Функция возвращает идентификатор потомка, который выполняет код. (Или, если вызов fork завершился неудачно, он просто бросает исключение.) Новый потомок завершает работу сразу после выполнения кода по ссылке.

Итак, вернёмся к вопросу - как запустить программу в фоновом режиме? Всё, что нужно сделать - это предотвратить выполнение того же кода, что и в потомке и вместо этого запустить нужную программу. Поскольку родитель продолжает работать, этого будет достаточно для достижения цели.

Perl позволяет достичь этого при помощи встроенной подпрограммы exec, которая похожа на system, за исключением того, что она заменяет запущенный в настоящее время код программой с указанным именем. exec - это Perl-интерфейс к системному вызову execve(2) (или к одному из его аналогов в языке C, например execv(3)). Легко представить, что exec - это system, приводящий к немедленному завершению работы и такое представление поможет начать им пользоваться. Но стоит сказать, что это не совсем правда. Например, exec не меняет идентификатор процесса.

Собрав эти части вместе, получим код для запуска программы в фоновом режиме, который будет выглядеть примерно так:
fork_child(sub {
    exec 'update_web_server', "--host=$host"
        or die "Не удалось выполнить update_web_server: $!\n";
});

Как можно получить статус завершения фоновой программы?

Одна из проблем приведённого выше кода заключается в том, что он не позволяет определить, с каким статусом завершилась фоновая программа (а в конце концов она завершится). Чтобы получить его, нужно будет дождаться завершения фоновой программы. Его позволяет получить функция wait, встроенная в Perl:
my $pid = fork_child(sub {
    exec 'update_web_server', "--host=$host"
        or die "Не удалось выполнить update_web_server: $!\n";
});

do_complicated_calculations(); # Выполнение сложных вычислений

waitpid $pid, 0;
waitpid, похожа на системный вызов waitpid(2) и является его обёрткой. Она приостанавливает текущий процесс до тех пор, пока не завершится потомок, идентификатор которого указан в первом аргументе, и возвращает идентификатор потомка. Perl помещает статус завершения потомка в переменной $?, как и при завершении функции system. Вы также можете указать идентификатор процесса -1, чтобы дождаться завершения любого из потомков.

Ожидание потомка иногда называют его жатвой (reaping).

Отметим, что с точки зрения ядра почти нет разницы между интерактивным и фоновым процессом. Единственное важное различие заключается в том, что родитель продолжает свою работу, перед тем как приступит к ожиданию потомков.

Что случится, если не читать статус завершения потомков?

Если вы породили фоновый процесс и не собираетесь ожидать его завершения, ядро оставит запись о процессе (и статус его завершения) на неопределённое время, потому что будет считать, что вы ещё можете затребовать его в будущем. Процессы, которые завершились, но статус завершения которых не был прочитан, называются процессами-зомби. Если у процесса на момент завершения ещё остаются процессы-зомби, эти процессы усыновляются: ядро меняет идентификатор их родителя на 1 (на большинстве Unix-систем - это init(8)), одна из задач которого заключается в чтении статуса завершения таких процессов-зомби.

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

Поскольку в данном случае потомки становятся зомби, единственный способ избежать их создания - удостовериться, что потомков нет. Как вариант - можно защитить свою программу, порождая внуков текущего процесса, выполняя второй системный вызов fork:
sub run_and_forget {
    my ($command, @arguments) = @_;

    my $child_pid = fork_child(sub {
        # Здесь - потомок
        fork_child(sub {
            # А тут - процесс-внук
            exec $command, @arguments
                or die "Не удалось выполнить $program: $!\n";
        });

    });

waitpid $child_pid, 0;
}
Исходный процесс порождает потомка и ожидает, когда тот завершит работу. Потомок немедленно порождает внука и завершает работу (так что исходный процесс может прочитать статус его завершения). Теперь родитель внуков более не существует, поэтому ядро передаёт их на усыновление процессу с идентификатором 1. Статус завершения потомка сразу считывается, а init(8) получает на усыновление внуков (поскольку у них больше нет родителя). Таким образом не остаётся процессов-зомби, занимающих ресурсы.

Как запустить несколько программ параллельно?

Учитывая возможность запуска программы в фоновом режиме, параллельный запуск нескольких программ довольно очевиден: всё, что нужно - это запустить их все в фоновом режиме и дождаться, когда они все завершат работу. Неплохо также запомнить идентификаторы порождённых процессов (чтобы, например, выводить сообщения о произошедших ошибках).
my %host_for_pid;
for my $host (hosts_to_update()) {
    my $pid = fork_child(sub {
        exec 'update_web_server', "--host=$host"
            or die "Не удалось выполнить update_web_server: $!\n";
        });
    $host_for_pid{$pid} = $host;
}

while (keys %host_for_pid) {
    my $pid = waitpid -1, 0;
    warn "Не удалось обновить узел $host_for_pid{$pid}\n"
        if $? != 0;
    delete $host_for_pid{$pid};
}

Как запустить программу в другом каталоге?

Раздельность fork и exec в Unix поначалу поражает людей излишней сложностью при запуске программ. Но в этом есть значительная выгода: потомок может изменить собственное окружение необходимым образом перед выполнением требуемой программы.

Приведём простой пример, в котором новая программа будет запущена в другом текущем каталоге. (Вообще, можно достичь того же результата поменяв текущий каталог в родителе перед запуском потомка, а затем вернуться в исходный каталог; но на практике такой подход чреват ошибками, поскольку исходный каталог в это время может быть переименован.)
my $pid = fork_child(sub {
chdir $dir
    or die "Не удалось перейти в каталог $dir: $!\n";
exec 'tar', '-cf', $tar_file, '.'
    or die "Не удалось выполнить tar: $!\n";
});

waitpid $pid, 0;

Как перехватить вывод программы?

Во многих случаях лучший способ перехватить вывод программы - воспользоваться встроенными возможностями Perl: косыми кавычками или довольно сложным открытием через оболочку. Например, так:
open my $ps, 'ps |' or die "Не удалось открыть на чтение команду ps: $!\n";
# Теперь можно читать из $ps, как из обычного файла Perl
Или без использования оболочки для вызова команды, что безопаснее если аргументы команды содержат данные из недоверенных источников:
open my $ps, '-|', 'ps', $pid or die "Не удалось открыть на чтение команду ps: $!\n";

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

Для перехвата вывода команды используется канал: однонаправленное соединение для простого поточно-ориентированного взаимодействия между процессами. Канал поддерживается ядром системы и выглядит для приложений как пара файловых дескрипторов. (Файловые дескрипторы Unix - это целые числа. Они являются низкоуровневыми эквивалентами файлов из Perl; на самом деле файл Perl можно рассматривать как простую обёртку вокруг файлового дескриптора с опциональной буферизацией.) Один из файловых дескрипторов предназначен для записи в канал, а другой - для чтения из канала; всё записанное в канал со стороны записи в конечном счёте появится на стороне для чтения.

Возможность создать канал хорошо сочетается с исключительно важной особенностью системного вызова fork, упомянутой ранее: потомки наследуют все открытые файлы своего родителя. Захват вывода процесса реализуется следующим образом:
  1. Родитель создаёт канал и получает пару новых файловых дескрипторов (обёрнутых в файлы Perl, если вы используете его),
  2. Затем порождает потомка, который наследует файловые дескрипторы канала,
  3. Потомок закрывает канал со стороны для чтения (поскольку он будет лишь писать данные),
  4. Потомок отправляет данные, записывая их в канал со стороны записи, возможно даже выполняя новую программу,
  5. Родитель закрывает канал со стороны для записи (поскольку ему нужно лишь читать данные),
  6. Родитель читает из канала, блокируясь до тех пор, пока не появятся данные от потомка,
  7. Когда операция чтения сообщает о конце файла, родитель считывает статус завершения потомка.
Здесь не отражена одна тонкость. В Unix принято, что по умолчанию программы пишут на свой стандартный вывод с файловым дескриптором 1. Но в подавляющем большинстве случаев у только что созданного канала конец, используемый для записи, не будет иметь файловый дескриптор, равный 1 (потому что это означало бы, что к моменту создания канала не был открыт ни стандартный ввод, ни стандартный вывод).

Поэтому нужен этап 3а, на котором потомок должен связать файловый дескриптор 1 с концом канала, используемым для записи. Проще всего это сделать при помощи системного вызова dup2(2), который связывает выбранный вами файловый дескриптор с другим файловым дескриптором, так что они оба становятся связаны с одним и тем же файлом. Когда потомок удвоит конец канала для записи, он может закрыть исходный файловый дескриптор (для порядка, особенно в том случае, когда потомок собирается выполнить другую программу).

К несчастью, Perl не предоставляет прямого доступа к dup2, но его можно импортировать из стандартного модуля POSIX. (Системный вызов dup очень похож на dup2, но воспользоваться им в наших целях сложнее. В Perl нет встроенной функции dup, но её свойствами обладает функция open.) Первый аргумент dup2 - номер файлового дескриптора, который нужно клонировать (и который можно получить из файла Perl при помощи встроенной функции fileno), а второй аргумент - номер файлового дескриптора, который должен стать клоном первого.

Совместив вместе код дублирования с примером, использующим ps, получим следующее:
use POSIX qw<dup2>;

# 1. Создаём оба конца канала
pipe my ($readable, $writable)
    or die "Не удалось создать канал: $!\n";

# 2. Создаём потомка
my $pid = fork_child(sub {
    # 3. Потомок закрывает конец канала, пригодный для чтения
    close $readable or die "Потомок не смог закрыть канал: $!\n";
    # 3a. Подключаем стандартный вывод к концу канала, пригодному для записи
    dup2(fileno $writable, 1)
        or die "Потомок не смог перенаправить стандартный вывод в канал: $!\n";
    close $writable or die "Потомок не смог закрыть канал: $!\n";
    # 4. Выполняем процесс, выводящий данные на стандартный вывод
    exec 'ps' or die "Не удалось выполнить ps: $!\n";
});

# 5. Родитель закрывает сторону канала, пригодную для записи
close $writable or die "Не удалось закрыть канал: $!\n";

# 6. Родитель читает из другого конца канала, блокируясь в ожидании данных
while (<$readable>) {
    print "Вывод ps: $_";
}
close $readable or die "Не удалось закрыть канал: $!\n";
# 7. Вывода больше не ожидается, считываем статус завершения потомка
waitpid $pid, 0;
die "ps завершился с ошибкой\n" if $? != 0;
Шаг 5 особенно важен, что не кажется очевидным. Когда родитель читает из канала, он блокируется в ожидании данных. Он не распознает события окончания файла, пока не будут закрыты все файловые дескрипторы канала, связанные с концом для записи (потому что пока они не закрыты, могут появиться новые данные). Поэтому если шаг 5 был бы пропущен, то когда потомок завершил бы работу (и закрыл бы свою копию дескриптора файла для записи), родитель по-прежнему был бы заблокирован в ожидании, когда будет закрыт его собственный дескриптор канала для записи.

Теперь можно отметить выгоды, которые приобретает Unix от разделения вызова программ на этапы fork и exec: потомок может выполнить произвольную подготовительную работу между двумя этапами.

Как отправлять данные в программу?

Как и в случае с захватом вывода, лучше всего воспользоваться стандартной для Perl возможностью "открыть команду":
open my $lpr, '| lpr'
    or die "Не удалось вывести данные в lpr: $!\n";
open my $lpr, '|-', 'lpr', '-H', $server
    or die "Не удалось вывести данные в lpr: $!\n";
И снова стоит обратить внимание на то, что происходит внутри. Как и ожидалось, происходит строго обратное захвату вывода программы. При этом потомок дублирует сторону канала для чтения, замещая ей стандартный ввод команды (с файловым дескриптором 0):
pipe my ($readable, $writable)
    or die "Не удалось создать канал: $!\n";

my $pid = fork_child(sub {
    close $writable or die "Потомок не смог закрыть канал: $!\n";
    dup2(fileno $readable, 0)
        or die "Потомок не смог перенаправить вывод канала на стандартный ввод: $!\n";
    close $readable or die "Потомок не смог закрыть канал: $!\n";
    exec 'lpr' or die "Не удалось выполнить lpr: $!\n";

});

close $readable or die "Не удалось закрыть канал: $!\n";
while (my $line = get_next_printer_line()) {
    print $writable $line;
}
close $writable or die "Не удалось закрыть канал: $!\n";
waitpid $pid, 0;
die "lpr завершился с ошибкой\n" if $? != 0;
Важно знать, что чтение из канала приводит к блокировке до тех пор, пока не появятся данные. Но и запись в канал может привести к блокировке, если с другой стороны канала его никто не читает. Данные, записанные в канал, ядро помещает в буфер до тех пор, пока кто-нибудь не попытается прочитать эти данные. Но размер буфера ограничен. Как только буфер заполнится, процесс, пытающийся писать в канал, будет приостановлен до тех пор, пока другой процесс не прочитает данные из буфера.

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

Как отправлять данные в процесс и захватывать его вывод?

Можно ли совместить в одном процессе захват вывода процесса и отправку на стандартный ввод этого процесса? Короткий ответ - да, но об этом стоит рассказать подробнее.

Для двунаправленного перенаправления в процесс-потомок нужны два канала, один - для отправки и второй - для захвата (поскольку каждый из них является однонаправленным каналом связи). Учитывая это, код кажется простым: нужно создать оба канала в родителе, выполнить ветвление, а в потомке присоединить каналы к стандартному вводу и стандартному выводу, прежде чем запустить нужную программу:
pipe my ($send_readable, $send_writable)
    or die "Не удалось открыть канал для отправки: $!\n";

pipe my ($capture_readable, $capture_writable)
    or die "Не удалось открыть канал для захвата: $!\n";

my $pid = fork_child(sub {
    close $send_writable or die "Не удалось закрыть канал: $!\n";
    close $capture_readable or die "Не удалось закрыть канал: $!\n";
    dup2(fileno $send_readable, 0)
        or die "Потомок не смог соединить канал со стандартным вводом: $!\n";
    dup2(fileno $capture_writable, 1)
        or die "Потомок не смог соединить канал со стандартным выводом: $!\n";
    close $send_readable or die "Не удалось закрыть канал: $!\n";
    close $capture_writable or die "Не удалось закрыть канал: $!\n";
    exec 'sort' or die "Не удалось запустить sort: $!\n";
});

close $send_readable or die "Не удалось закрыть канал: $!\n";
close $capture_writable or die "Не удалось закрыть канал: $!\n";
print {$send_writable} get_data();
close $send_writable or die "Не удалось закрыть канал: $!\n";
while (my $line = <$capture_readable>) {
    print $line;
}
waitpid $pid, 0;
die "sort завершился неудачно\n" if $? != 0;
Конкретно этот пример написан аккуратно, так что будет правильно работать вне зависимости от того, что вернёт get_data. Но если, скажем, просто заменить программу, запускаемую в потомке, могут возникнуть серьёзные проблемы.

Этот код пишет всю информацию на вход sort перед тем, как начать что-то читать на выходе из sort'а. Но sort поглощает все данные. И только когда родитель закрывает пригодный для записи конец канала для отправки данных, sort понимает, что данные закончились и приступает к сортировке. Затем родитель начинает чтение из канала для захвата данных. Если данных много, то на этом месте он даже может заблокироваться в ожидании данных, потому что для сортировки большого объёма данных нужно много времени. В конце концов sort заканчивает самую трудную часть работы и начинает выдавать результат, а родитель - читать отсортированные данные.

Проблема в том, что канал использует буфер постоянного размера. Представим, что вместо sort(1) в примере выше запускается программа-фильтр вроде tr(1). Важное отличие между sort и tr заключается в том, что для выполнения своей задачи sort должен прочитать свой ввод целиком, прежде чем он сможет сгенерировать вывод (потому что последние строки могут попасть в начало), в то время как tr может легко генерировать вывод по мере поступления данных.

Родитель в этом примере попытается что-то записать, прежде чем начать читать. Как только начнётся запись, потомок с tr прочитает со своего конца канала данные и сгенерирует вывод, который должен быть прочитан через канал для захвата данных. Запись в канал для захвата данных рано или поздно вызовет проблему: канал заполнится, ядро заблокирует процесс tr, прежде чем он сможет записать следующую порцию данных, до тех пор, пока они не будут прочитаны с другого конца канала. Но единственный процесс, который может прочитать данные - это родитель, а он всё ещё ждёт когда освободится канал для записи данных. Вот так:
  • Никто не читает из канала для захвата данных,
  • Поэтому потомок tr не может записать данные в этот канал,
  • Поэтому никто больше не читает данные из канала для отправки данных,
  • Поэтому родитель остаётся заблокированным, потому что по-прежнему пытается писать в канал для отправки данных,
  • … но пока он не отправит все данные, он не станет читать из канала для захвата данных!
Это классический пример взаимоблокировки: два процесса заблокированы в ожидании когда другой освободит общий ресурс. И есть только один выход из этого состояния - прервать один или оба процесса сигналом.

Есть способы безопасной работы с двойными каналами. Первый - это использование программ, похожих на sort, которые считывают весь ввод прежде чем сгенерировать вывод. Второй - если вы полностью уверены в том, что активность в двух каналах будет чередоваться таким образом, что взаимоблокировка никогда не произойдёт. Например, если соблюдать осторожность, можно использовать пару каналов для работы с dc(1), калькулятором произвольной точности с постфиксной нотацией. Это возможно поскольку вы можете предсказать, какой вывод будет сгенерирован на отправленную команду; если вы отправите такую команду, вы можете прервать запись в канал отправки и прочитать правильное количество строк из канала для захвата вывода.

Иногда проблему можно обойти, породив два процесса, а не один. Один будет выполнять требуемую программу; а другой будет клоном родителя, который будет заниматься только генерацией данных, подаваемых на вход программы-фильтра:
pipe my ($send_readable, $send_writable)
    or die "Не удалось создать канал для отправки: $!\n";
pipe my ($capture_readable, $capture_writable)
    or die "Не удалось создать канал для захвата: $!\n";

my $writer_pid = fork_child(sub {
    close $capture_readable or die "Не удалось закрыть канал: $!\n";
    close $capture_writable or die "Не удалось закрыть канал: $!\n";
    close $send_readable or die "Не удалось закрыть канал: $!\n";
    print {$send_writable} get_data();
    close $send_writable or die "Не удалось закрыть канал: $!\n";
});

my $filter_pid = fork_child(sub {
    close $send_writable or die "Не удалось закрыть канал: $!\n";
    close $capture_readable or die "Не удалось закрыть канал: $!\n";
    dup2(fileno $send_readable, 0)
        or die "Потомок не смог соединить стандартный ввод с каналом: $!\n";
    dup2(fileno $capture_writable, 1)
        or die "Потомок не смог соединить стандартный вывод с каналом: $!\n";
    close $send_readable or die "Не удалось закрыть канал: $!\n";
    close $capture_writable or die "Не удалось закрыть канал: $!\n";
    exec 'tr', 'a-z', 'A-Z'
        or die "Не удалось выполнить tr: $!\n";
});

close $send_readable or die "Не удалось закрыть канал: $!\n";
close $send_writable or die "Не удалось закрыть канал: $!\n";
close $capture_writable or die "Не удалось закрыть канал: $!\n";
print while <$capture_readable>;

while (1) {
    my $pid = waitpid -1, 0;
    last if $pid == -1;
    next if $? == 0;
    my $process = $pid == $writer_pid ? 'писатель'
                : $pid == $filter_pid ? 'фильтр'
                : 'другой';
    warn "Процесс $process завершился неудачно\n";
}
Однако на практике это решение может оказаться неуклюжим, потому что процесс, порождающий вывод, не догадывается о связи с родителем. В частности, код процесса, порождающего вывод, не сможет изменить переменные родителя.

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

Ваше приложение должно принимать решение, нужно ли использовать временный файл для ввода или вывода, оценив ожидаемый объём данных, передаваемых в каждом из направлений. В следующем примере используется временный файл для отправки данных в фильтр:
use File::Temp qw<tempfile>;

my $temp_fh = tempfile();
print {$temp_fh} get_data();
seek $temp_fh, 0, 0 or die "Не удалось вернуться в начало временного файла: $!\n";

pipe my ($readable, $writable)
    or die "Не удалось создать канал: $!\n";

my $pid = fork_child(sub {
    close $readable or die "Потомок не смог закрыть сторону канала для чтения: $!\n";
    dup2(fileno $temp_fh, 0)
        or die "Потомок не смог присоединить стандартный ввод ко временному файлу: $!\n";
    dup2(fileno $writable, 1)
        or die "Потомок не смог присоединить стандартный вывод к каналу: $!\n";
    exec 'tr', 'a-z', 'A-Z'
        or die "Не удалось выполнить tr: $!\n";
});

close $writable or die "Не удалось закрыть канал: $!\n";
print while <$readable>;

waitpid $pid, 0;
die "tr завершился неудачно\n" if $? != 0;
Здесь используется функция tempfile из модуля File::Temp, которая по умолчанию создаёт временный файл и немедленно удаляет его. Тут используется следующая особенность Unix: когда файл удалён по имени, его данные остаются нетронутыми до тех пор, пока не останется ни одного процесса, открывшего этот файл. В данном случае потомок унаследует соответствующий файловый дескриптор, так что данные во временном файле не удалятся до тех пор, пока оба процесса не закроют файл.

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

Как захватить вывод программы в поток диагностики?

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

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

Захват стандартного вывода и потока диагностики легко объединить: для этого нужно подсоединить в потомке и stdout и stderr к каналу ввода-вывода:
pipe my ($readable, $writable)
    or die "Не удалось создать канал ввода-вывода: $!\n";

my $pid = fork_child(sub {
    close $readable or die "Потомку не удалось закрыть канал ввода-вывода со стороны для чтения: $!\n";
    dup2(fileno $writable, 1)
        or die "Потомку не удалось подсоединить стандартный вывод к каналу ввода-вывода: $!\n";
    dup2(fileno $writable, 2)
        or die "Потомку не удалось подсоединить поток диагностики к каналу ввода-вывода: $!\n";
    close $writable or die "Потомку не удалось закрыть канал ввода-вывода со стороны для записи: $!\n";
    exec $program, @args or die "Не удалось запустить $program: $!\n";
});

close $writable or die "Не удалось закрыть канал ввода-вывода: $!\n";
print while <$readable>;
close $readable or die "Не удалось закрыть канал ввода-вывода: $!\n";

waitpid $pid, 0;
die "Программа $program завершилась неудачно\n" if $? != 0;
С другой стороны, захват двух отдельных потоков значительно сложнее: обычный подход (с использованием одного канала для стандартного вывода и другого для потока диагностики) чреват рисками взаимоблокировки. Проблема в том, что родитель должен определить, из какого из двух каналов нужно читать в данный момент: если попытаться прочитать из одного, когда в нём нет данных, программа заблокируется; в то время как потомок не сможет записать данные в другой канал, пока он заблокирован.

Один из подходов справиться с этим - читать из заданного канала только когда ядро посчитает, что канал готов для чтения. В Perl это обычно означает использование стандартного модуля IO::Select или, возможно, встроенной функции select с четырьмя аргументами, обёрткой над которой и является этот модуль. Идея заключается в том, что вместо простого чтения данных из дескриптора файла, сначала вызывается select со всеми интересующими нас файловыми дескрипторами. Произойдёт ожидание, пока хотя бы один из этих файловых дескрипторов не перейдёт в состояние готовности, после чего будет возвращён список готовых дескрипторов. После этого можно читать из всех них, повторяя цикл до тех пор, пока все файловые дескрипторы не достигнут конца файла.

Большой минус при использовании select заключается в том, что больше нельзя использовать обычный оператор Perl <> для чтения из файла. Вместо этого нужно использовать встроенную функцию sysread, потому что <> буферизует ввод, что не сочетается с необходимостью чтения из файловых дескрипторов лишь в тех случаях, когда они к этому готовы.

Зная это, во многих обстоятельствах проще бывает воспользоваться временным файлом для одного из дескрипторов - например, для потока диагностики, предполагая, что сообщений об ошибках не будет настолько много, что возникнет проблема нехватки места на диске.
my $temp_stderr = tempfile();

pipe my ($readable, $writable)
    or die "Не удалось создать канал: $!\n";

my $pid = fork_child(sub {
    close $readable or die "Потомок закрыл пригодный для чтения канал: $!\n";
    dup2(fileno $writable, 1)
        or die "Потомку не удалось подсоединить стандартный вывод к каналу ввода-вывода: $!\n";
    dup2(fileno $temp_stderr, 2)
        or die "Потомку не удалось подсоединить поток диагностики ко временному файлу: $!\n";
    exec $program, @arguments
        or die "Не удалось запустить $program: $!\n";
});

close $writable or die "Не удалось закрыть канал ввода-вывода: $!\n";

# Обработка данных со стандартного вывода потомка
print "stdout: $_" while <$readable>;

waitpid $pid, 0;
die "Программа $program завершилась неудачно\n" if $? != 0;

# Теперь потомок завершился, поэтому данных в $temp_stderr больше не будет
seek $temp_stderr, 0, 0;

# Обработка данных с потока диагностики потомка
print "stderr: $_" while <$temp_stderr>;

Как можно передать несколько открытых файлов в процесс-потомок?

Предположим, что вы хотите запустить потомка, стандартный ввод, стандартный вывод и поток диагностики которого подключены туда же, куда подключен родитель. Но родитель также получит доступ к другим открытым файлам, в том числе ко временным файлам. Звучит довольно просто. Возможно, потомок должен получить дескриптор файла в качестве аргумента, тогда он сможет узнать, где найти дополнительный файл; если записать это на Perl, то с помощью open можно создать файл Perl, соответствующий дескриптору файла:
my $file_descriptor = $ARGV[0];
open my $temp_fh, "<&=$file_descriptor"
    or die "Не удалось открыть дескриптор $file_descriptor: $!\n";

Затем родителю нужно открыть временный файл и просто выполнить fork/exec для порождения потомка, верно?
my $temp_fh = tempfile();
fork_child(sub {
    exec 'use_tempfile', fileno $temp_fh
        or die "Не удалось запустить use_tempfile: $!\n";
});
К сожалению, это не работает: вы просто получите сообщение "Не удалось открыть дескриптор 3: Плохой файловый дескриптор" или что-то подобное. Причина в том, что File::Temp явным образом создаёт временные файлы недоступными для потомков. (Это происходит потому, что по соображениям безопасности эта настройка в File::Temp используется по умолчанию, но всё-таки это было неожиданным.)

Это делается настройкой флагов дескриптора файла, к которым надо добавить флаг FD_CLOEXEC — он сообщает ядру о необходимости закрыть дескриптор файла, если будет запущена новая программа. Таким образом, потомок, выполнивший вызов fork, изначально имеет доступ ко временному файлу, но дескриптор этого файла будет закрыт в потомке при успешном выполнении exec. (FD_CLOEXEC означает “file descriptor close on exec” - "файловый дескриптор закрыть при exec".)

Тем временем родитель ничего не выполнял, поэтому его версия файлового дескриптора не меняется.

Итак, чтобы передать открытый файл в процесс-потомок, нужно явным образом удалить флаг FD_CLOEXEC у дескриптора временного файла. Для безопасности нужно сначала получить текущие флаги файлового дескриптора, убрать из них флаг FD_CLOEXEC и только потом установить новое значение флагов файлового дескриптора:
use POSIX qw<F_SETFD F_GETFD FD_CLOEXEC>;

my $flags = fcntl $temp_fh, F_GETFD, 0;
$flags &= ~FD_CLOEXEC;
fcntl $temp_fh, F_SETFD, $flags;
Это можно сделать либо в потомке перед выполнением новой программы, либо в родителе; в частности, последний способ может оказаться полезным, если нужно передать один и тот же открытый файл нескольким разным потомкам.

Возникает вопрос, как же работали все предыдущие примеры, хотя они этого не делали? Ответ заключается в том, что флаги вроде FD_CLOEXEC привязаны к определённым номерам дескрипторов файлов. Код выше делал примерно следующее:
my $temp_fh = tempfile();

fork_child(sub {
    dup2(fileno $temp_fh, 1);
    exec $program, @arguments;
});
В этом случае дескриптор файла, скрывающийся в файле, возвращаемом tempfile(), закрывался при выполнении программы $program. Но к этому моменту он уже был склонирован как файловый дескриптор 1, а FD_CLOEXEC применяется только к fileno $temp_fh, но не к клону.

воскресенье, 18 июня 2017 г.

Flash-плагин для Chromium и Iceweasel/Firefox в Debian

В 2012 году Adobe объявил о прекращении выхода новых версий Flash-плагина для Linux: 01.04.2012 14:29 Релиз Adobe Flash 11.2, последней версии с поддержкой Linux. Мне этот плагин не нужен, но вот люди, любящие играть во Flash-игры под Linux в социальных сетях, опечалились - многие игры отказываются работать в старых версиях плагина.

Надо сказать, что браузер Iceweasel/Firefox, являющийся далёким потомком браузера Netscape, предоставляет интерфейс для подключения к нему плагинов. Этот интерфейс называется NPAPI - Netscape Plugin Application Programming Interface - программный интерфейс подключаемых модулей Netscape.

У браузера Chromium, являющегося свободной версией браузера Chrome, разрабатываемого Google, имеется собственный интерфейс для подключения плагинов. В Google решили не использовать NPAPI, посчитав что в нём имеется масса проблем и нет нужных функций. Взамен Google предложили новый интерфейс - PPAPI - Pepper Plugin Application Program Interface - программный интерфейс подключаемых модулей Pepper.

Когда Adobe объявил, что новые версии Flash-плагина для Linux выпускаться не будут, это объявление не касалось Flash-плагина, который выпускался Google. Google продолжил поддержку выпуска Flash-плагина в варианте PPAPI для своего браузера Chrome. Этот плагин можно было использовать и в Linux.

И вот в прошлом году я прочитал такие две новости: 05.09.2016 16:52 Adobe возобновляет выпуск NPAPI-плагина с Flash Player для Linux, 20.12.2016 12:10 Релиз Adobe Flash 24 для Linux. "Неплохо" - сказал я и решил попробовать обновить Flash-плагин. На тот момент плагины без проблем обновились штатными средствами.

1.1. Штатная установка Flash-плагина в Chromium

Штатно Flash-плагин для Chromium устанавливается так:
# apt-get install pepperflashplugin-nonfree
# update-pepperflashplugin-nonfree --install

1.2. Штатная установка Flash-плагина в Iceweasel/Chromium

Для Iceweasel/Firefos штатно Flash-плагин устанавливается так:
# apt-get install flashplugin-nonfree
# update-flashplugin-nonfree --install

2.1. Проблемы при установке Flash-плагина в Chromium

Я бы и не стал писать эту небольшую заметку, если бы штатные средства продолжали бы работать и сейчас. Но, к сожалению, сейчас они работать перестали. Например, при попытке обновить Flash-плеер PPAPI для Chromium выдаются такие ошибки:
# update-pepperflashplugin-nonfree --install
--2017-03-28 22:28:55--  http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_57.0.2987.110-1_amd64.deb
Распознаётся dl.google.com (dl.google.com)… 209.85.233.190, 209.85.233.91, 209.85.233.136, ...
Подключение к dl.google.com (dl.google.com)|209.85.233.190|:80... соединение установлено.
HTTP-запрос отправлен. Ожидание ответа... 200 OK
Длина: 47312644 (45M) [application/x-debian-package]
Сохранение в: «/tmp/pepperflashplugin-nonfree.VjH7fdoXcK/google-chrome-stable_57.0.2987.110-1_amd64.deb»

     0K .......... .......... .......... .......... ..........  0%  483K 95s
------------------8<------------------8<------------------8<------------------
 46150K .......... .......... .......... .......... .......... 99% 5,47M 0s
 46200K ...                                                   100% 7160G=11s

2017-03-28 22:29:07 (4,01 MB/s) - «/tmp/pepperflashplugin-nonfree.VjH7fdoXcK/google-chrome-stable_57.0.2987.110-1_amd64.deb» сохранён [47312644/47312644]

mv: не удалось выполнить stat для «unpackchrome/opt/google/chrome/PepperFlash/libpepflashplayer.so»: Нет такого файла или каталога

2.2. Проблемы при установке Flash-плагина в Iceweasel/Firefox

При попытке обновить Flash-плагин для Iceweasel/Firefox происходит другая ошибка:
# update-flashplugin-nonfree --install
--2017-03-28 22:48:07--  https://fpdownload.adobe.com/get/flashplayer/pdc/24.0.0.186/flash_player_npapi_linux.x86_64.tar.gz
Распознаётся fpdownload.adobe.com (fpdownload.adobe.com)… 2.17.215.70
Подключение к fpdownload.adobe.com (fpdownload.adobe.com)|2.17.215.70|:443... соединение установлено.
HTTP-запрос отправлен. Ожидание ответа... 404 Not Found
2017-03-28 22:48:07 ОШИБКА 404: Not Found.

ERROR: wget failed to download https://fpdownload.adobe.com/get/flashplayer/pdc/24.0.0.186/flash_player_npapi_linux.x86_64.tar.gz
More information might be available at:
  http://wiki.debian.org/FlashPlayer

3.1. Решение проблемы с установкой Flash-плагина в Chromium

Чинить будем в том же порядке. Во-первых, чтобы починить Flash-плагин для Chromium, из браузера Chromium заходим на официальную страницу Adobe, переходим по ссылке внизу Продукты, справа в списке "Загрузить" переходим по ссылке Adobe Flash Player, в выпадающем списке выбираем формат ".tar.gz для Linux" и жмём на кнопку "Загрузить". Извлечём из скачанного архива с именем "flash_player_ppapi_linux.x86_64.tar.gz" интересующий нас файл динамической библиотеки и поместим его в нужное место:
# tar xzvf flash_player_ppapi_linux.x86_64.tar.gz libpepflashplayer.so
# mv libpepflashplayer.so /usr/lib/pepperflashplugin-nonfree/
# chown root:root /usr/lib/pepperflashplugin-nonfree/libpepflashplayer.so
# chmod u=rw,go=r /usr/lib/pepperflashplugin-nonfree/libpepflashplayer.so

3.2. Решение проблемы с установкой Flash-плагина в Iceweasel/Firefox

Теперь точно такие же действия можно проделать из браузера Iceweasel/Firefox. В результате должен скачаться архив с именем "flash_player_npapi_linux.x86_64.tar.gz". Поступаем с ним аналогично - извлекаем интересующий нас файл динамической библиотеки и помещаем его в нужное место. В случае Iceweasel кладём файл в каталог /usr/lib/flashplugin-nonfree/:
# tar xzvf flash_player_npapi_linux.x86_64.tar.gz libflashplayer.so
# mv libflashplayer.so /usr/lib/flashplugin-nonfree/libflashplayer.so
# chown root:root /usr/lib/flashplugin-nonfree/libflashplayer.so
# chmod u=rw,go=r /usr/lib/flashplugin-nonfree/libflashplayer.so
В случае Firefox кладём файл в каталог /usr/lib/mozillz/plugins/:
# tar xzvf flash_player_npapi_linux.x86_64.tar.gz libflashplayer.so
# mv libflashplayer.so /usr/lib/mozilla/plugins/libflashplayer.so
# chown root:root /usr/lib/mozilla/plugins/libflashplayer.so
# chmod u=rw,go=r /usr/lib/mozilla/plugins/libflashplayer.so

4. Использование PPAPI-плагина в Iceweasel/Firefox

Когда Adobe объявила о прекращении поддержки NPAPI-плагина для Linux, наш соотечественник Ринат Ибрагимов - программист из Казани, занялся разработкой специального адаптера интерфейсов, который бы позволил использовать PPAPI-плагин в браузерах, предоставляющих NPAPI-интерфейс. Проект называется FreshPlayer и устанавливается в браузер как NPAPI-плагин. Вместо использования git-репозитория и сборки адаптера из исходных текстов мы воспользуемся готовым репозиторием для Debian Jessie. Добавим в файл /etc/apt/sources.list такую строчку:
deb http://http.debian.net/debian jessie-backports main contrib
Теперь обновим списки пакетов, доступных для установки из репозиториев:
# apt-get update
И установим пакет с адаптером:
# apt-get install install browser-plugin-freshplayer-pepperflash

Заключение

Как правило, Flash-плагин без проблем работает в Chromium. А вот с Iceweasel/Firefox могут возникнуть сложности. Например, в Iceweasel плагин NPAPI у меня не заработал, зато заработал PPAPI-плагин через адаптер FreshPlayer. В Firefox заработали оба варианта - и плагин NPAPI и плагин PPAPI через адаптер FreshPlayer. Если не получается заставить работать первый попавшийся вариант - попробуйте другой: попробуйте сначала плагин NPAPI, затем попробуйте обновить Iceweasel до Firefox, а если это не поможет - вместо плагина NPAPI можно попробовать воспользоваться плагином PPAPI через FreshPlayer.

воскресенье, 11 июня 2017 г.

Настройка zswap в Debian

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

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

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

Модули ядра для поддержки LZ4

Для начала впишем в файл /etc/initramfs-tools/modules модули ядра, реализующие быстрый алгоритм сжатия LZ:
lz4
lz4_compress
И пересоберём образ загрузочной файловой системы, чтобы в него попали добавленные нами модули:
# update-initramfs -u -k all

Модуль ядра для сжатия страниц подкачки

Теперь прописываем автоматическое включение zswap при загрузке ядра в конфигурации загрузчика GRUB в файле /etc/default/grub. Для этого добавим ряд новых настроек в GRUB_CMDLINE_LINUX_DEFAULT:
zswap.enabled=1 zswap.compressor=lz4 zswap.max_pool_percent=80
Значение настроек:
  • zswap.enabled=1 - включает использование zswap,
  • zswap.compressor=lz4 - выбирает более быстрый алгоритм сжатия lz4 вместо алгоритма сжатия по умолчанию lzo,
  • zswap.max_pool_percent=80 - разрешает использовать до 80 процентов оперативной памяти для хранения сжатых данных.
После изменения конфигурации загрузчика обновим его автоматически генерируемые файлы с настройками:
# update-grub
Всё готово, теперь нужно перезагрузить систему. Очень редкое действие, но этом случае перезагрузка действительно необходимо, т.к. загрузить модули без перезагрузки мне лично не удалось:
# reboot

Проверка

После перезагрузки можно проверить, работает ли модуль и какой алгоритм сжатия используется:
$ dmesg | grep zswap:
[    0.830940] zswap: loading zswap
[    0.835122] zswap: using lz4 compressor

Использованные материалы