Привет, Хабр! Сегодня немного поговорим про кроссплатформенную разработку, а именно – на языке Python.
Язык Python сам по себе считается кросс-платформенным, но до тех пор, пока дело не доходит до взаимодействия python-скрипта и внешних платформозависимых компонентов. Например, механизм подключения сетевой папки в Windows и Linux кардинально отличается. И если Вы пишите кросс-платформенный скрипт или даже библиотеку, то без организации кросс-платформенного кода на самом высоком уровне абстракции вам не обойтись.
Черпать идеи мы будем из самой кросс-платформенной библиотеки среди всех библиотек языка Python — os.py. В ней импортируются низкоуровневые модули в зависимости от состава стандартной библиотеки. Для разных платформ набор стандартных модулей интерпретатора Python разный. Вы можете в этом убедиться, посмотрев код модуля os.
Как видите, кортеж names содержит имена всех доступных модулей. Переменная модуля __all__ отвечает, какие объекты будут доступны при импорте c помощью инструкции from os import *. Если доступен модуль «posix», то выполняем специфичный для posix платформ код, а также прописываем ссылки на его доступные объекты в os.__all__. Затем удаляем объект posix, чтобы он был не доступен по вызову os.posix, так как он нам больше не нужен.
Таким образом, все объекты модуля posix доступны для вызова с помощью os.объект_модуля_posix. Аналогично и для Windows, только вместо posix — nt.
Следует отметить, что в Windows версии интерпретатора доступен только модуль nt. Posix, solaris и другие модули платформ не входят в состав поставки. Аналогично и для других платформ. Тот факт, что интерпретатор скомпилирован под определенную платформу, не вписывается в идею кроссплатформенности. Поэтому дистрибутив нашей библиотеки содержит модули для всех платформ.
Приступим к написанию собственной кросс-платформенной библиотеки. Для примера, напишем библиотеку, которая будет подключать сетевую папку независимо от платформы. Давайте посмотрим на команды, с помощью которых происходит подключение сетевой папки в разных операционных системах:
В Linux:
В Mac OSX:
В windows:
Не такие уж и разные команды. Как видите, каждая команда имеет в своем синтаксисе server, share, user и password. Все, что нам нужно, это определить платформу и выполнить функцию с входными параметрами server, share, user, password, mount_point (для win это будет буква диска).
Структура файлов для нашей библиотеки будет следующей:
Этот файл выполняет 2 функции:
Содержит общие для всех платформ объекты и прототипы классов, на основе которых будут построены платформозависмые классы.
(UPD: По замечанию пользователя hellman стоит отметить, что использовать shell=True не безопасно. Чтобы не усложнять пример, пожертвуем некоторой безопасностью, ради наглядности).
Здесь класс MounterBase является прототипом будущих платформозависимых классов из файлов win.py, osx.py, linux.py и пока ничего полезного не умеет, так как команды для монтирования и размонтирования не определенны. А теперь рассмотрим один из платформозависымых модулей.
В этом классе мы описываем специфичные параметры для платформы Linux, а именно команды монтирования и размонтирования. Сделаем аналогично для Мака и Windows.
В итоге, чтобы подключить сетевую папку, нам достаточно вызвать метод mount() экземпляра класса mounter.Mounter():
Теперь не нужно помнить синтаксис команд для всех платформ. Конечно, мы реализовали самый примитивный механизм подключения сетевой папки. В этом механизме не учитывается, что сетевая папка может быть без пароля, а также другие особенности. Реализацию этих особенностей я оставлю вам.
Подобную архитектуру мы используем в автоматическом тестировании продуктов Acronis True Image for Windows и Acronis True Image for Mac. Например, чтобы выполнить процедуру бекапа, достаточно вызвать функцию TrueImage.backup(), и в зависимости от платформы, на которой был запущен скрипт, будет выполнятся соответствующий платформозависимый код.
Предыдущие посты у нас в блоге:
– Мультиплатформенная разработка True Image
– 50 оттенков синего, или сказ о том, как мы делали дизайн True Image 2015
– Acronis Snap Deploy 5: Массовый деплоймент быстро просто и надёжно
– «Ни единого разрыва!» или зачем клиенту воевать с техподдержкой
– Стадии рождения новой функциональности в программном продукте
– Золотое правило бэкапа
Язык Python сам по себе считается кросс-платформенным, но до тех пор, пока дело не доходит до взаимодействия python-скрипта и внешних платформозависимых компонентов. Например, механизм подключения сетевой папки в Windows и Linux кардинально отличается. И если Вы пишите кросс-платформенный скрипт или даже библиотеку, то без организации кросс-платформенного кода на самом высоком уровне абстракции вам не обойтись.
Черпать идеи мы будем из самой кросс-платформенной библиотеки среди всех библиотек языка Python — os.py. В ней импортируются низкоуровневые модули в зависимости от состава стандартной библиотеки. Для разных платформ набор стандартных модулей интерпретатора Python разный. Вы можете в этом убедиться, посмотрев код модуля os.
_names = sys.builtin_module_names
# Note: more names are added to __all__ later.
__all__ = ["altsep", "curdir", "pardir", "sep", "extsep", "pathsep", "linesep",
"defpath", "name", "path", "devnull",
"SEEK_SET", "SEEK_CUR", "SEEK_END"]
def _get_exports_list(module):
try:
return list(module.__all__)
except AttributeError:
return [n for n in dir(module) if n[0] != '_']
if 'posix' in _names:
name = 'posix'
linesep = '\n'
from posix import *
try:
from posix import _exit
except ImportError:
pass
import posixpath as path
import posix
__all__.extend(_get_exports_list(posix))
del posix
Как видите, кортеж names содержит имена всех доступных модулей. Переменная модуля __all__ отвечает, какие объекты будут доступны при импорте c помощью инструкции from os import *. Если доступен модуль «posix», то выполняем специфичный для posix платформ код, а также прописываем ссылки на его доступные объекты в os.__all__. Затем удаляем объект posix, чтобы он был не доступен по вызову os.posix, так как он нам больше не нужен.
Таким образом, все объекты модуля posix доступны для вызова с помощью os.объект_модуля_posix. Аналогично и для Windows, только вместо posix — nt.
Следует отметить, что в Windows версии интерпретатора доступен только модуль nt. Posix, solaris и другие модули платформ не входят в состав поставки. Аналогично и для других платформ. Тот факт, что интерпретатор скомпилирован под определенную платформу, не вписывается в идею кроссплатформенности. Поэтому дистрибутив нашей библиотеки содержит модули для всех платформ.
Приступим к написанию собственной кросс-платформенной библиотеки. Для примера, напишем библиотеку, которая будет подключать сетевую папку независимо от платформы. Давайте посмотрим на команды, с помощью которых происходит подключение сетевой папки в разных операционных системах:
В Linux:
mount //server/share /mount_point -o user=user,pass=password
В Mac OSX:
mount_smbfs //user:password@server/share /mount_point
В windows:
net use z: \\server\share /user:user password
Не такие уж и разные команды. Как видите, каждая команда имеет в своем синтаксисе server, share, user и password. Все, что нам нужно, это определить платформу и выполнить функцию с входными параметрами server, share, user, password, mount_point (для win это будет буква диска).
Структура библиотеки
Структура файлов для нашей библиотеки будет следующей:
__init__.py
Этот файл выполняет 2 функции:
- Определяет, что папка mounter является пакетом
- Импортирует в область видимости пакета mounter, объекты в зависимости от платформы
if sys.platform == "win32":
from mounter.win import *
if sys.platform == "darwin":
from mounter.osx import *
if sys.platform == "linux2":
from mounter.linux import *
base.py
Содержит общие для всех платформ объекты и прототипы классов, на основе которых будут построены платформозависмые классы.
import os
from subprocess import Popen, PIPE
class MounterBase():
mount_cmd = None
umount_cmd = None
mount_point = None
network_folder = None
def _prepare(self):
if not os.path.exists(self.mount_point):
print 'creating %s folder for mounting' % self.mount_point
os.makedirs(self.mount_point)
def mount(self):
self._prepare()
Popen(self.mount_cmd, stdout=PIPE, stderr=PIPE, shell=True)
def umount(self):
Popen(self.umount_cmd, stdout=PIPE, stderr=PIPE, shell=True)
(UPD: По замечанию пользователя hellman стоит отметить, что использовать shell=True не безопасно. Чтобы не усложнять пример, пожертвуем некоторой безопасностью, ради наглядности).
Здесь класс MounterBase является прототипом будущих платформозависимых классов из файлов win.py, osx.py, linux.py и пока ничего полезного не умеет, так как команды для монтирования и размонтирования не определенны. А теперь рассмотрим один из платформозависымых модулей.
linux.py
import mounter
class Mounter(mounter.MounterBase):
def __init__(self, network_folder, mount_point, user, password):
mount_cmd = "mount {network_folder} {mount_point} -o user={user},pass={password}"
self.mount_cmd = mount_cmd.format(network_folder=network_folder,
mount_point=mount_point,
user=user,
password=password)
self.umount_cmd = "umount {mount_point}".format(mount_point=mount_point)
В этом классе мы описываем специфичные параметры для платформы Linux, а именно команды монтирования и размонтирования. Сделаем аналогично для Мака и Windows.
osx.py
class Mounter(mounter.MounterBase):
def __init__(self, network_folder, mount_point, user, password):
mount_cmd = "mount_smbfs //{user}:{password}@{network_folder} {mount_point}"
self.mount_cmd = mount_cmd.format(network_folder=network_folder,
mount_point=mount_point,
user=user,
password=password)
self.umount_cmd = "umount {mount_point}".format(mount_point=mount_point)
win.py
class Mounter(mounter.MounterBase):
def __init__(self, network_folder, mount_point, user, password):
network_folder =network_folder.replace("/", "\\")
mount_cmd = "net use {mount_point} \\{network_folder} /user:{user} {password}"
self.mount_cmd = mount_cmd.format(network_folder=network_folder,
mount_point=mount_point,
user=user,
password=password)
self.umount_cmd = "net use {mount_point} /delete /y".format(mount_point=mount_point)
В итоге, чтобы подключить сетевую папку, нам достаточно вызвать метод mount() экземпляра класса mounter.Mounter():
import mounter
...
mount_point = "/mnt/mount_moint"
share = mounter.Mounter("server/share", mount_point, "guest", "secret_password")
share.mount()
copy_requared_files(source=mount_point, dest="/tmp")
share.umount()
Теперь не нужно помнить синтаксис команд для всех платформ. Конечно, мы реализовали самый примитивный механизм подключения сетевой папки. В этом механизме не учитывается, что сетевая папка может быть без пароля, а также другие особенности. Реализацию этих особенностей я оставлю вам.
Подобную архитектуру мы используем в автоматическом тестировании продуктов Acronis True Image for Windows и Acronis True Image for Mac. Например, чтобы выполнить процедуру бекапа, достаточно вызвать функцию TrueImage.backup(), и в зависимости от платформы, на которой был запущен скрипт, будет выполнятся соответствующий платформозависимый код.
Предыдущие посты у нас в блоге:
– Мультиплатформенная разработка True Image
– 50 оттенков синего, или сказ о том, как мы делали дизайн True Image 2015
– Acronis Snap Deploy 5: Массовый деплоймент быстро просто и надёжно
– «Ни единого разрыва!» или зачем клиенту воевать с техподдержкой
– Стадии рождения новой функциональности в программном продукте
– Золотое правило бэкапа
Комментарии (5)
hellman
13.04.2015 16:35+30Предлагаю под линуксом примонтировать папку "; rm -rf /;". И больше не использовать Popen(shell=True)
vit1251
14.04.2015 01:59Тов. hellman прав. Ваши пользователи могут оказаться тем еще Дропиками. Так что рекомендую использовать subprocess, а уже в нем указывать напрямую executable="/bin/mount".
den-gts Автор
14.04.2015 08:17Согласен. Стоило упомянуть в статье, что использование Popen(shell=True) упрощает код, но не безопасно.
nightvich
Спасибо за пример «на пальцах». Очень актуально!
nightvich
Я все понял. Прошу прощения.