Перейти к основному контенту

Структура апплетов RemoteApp и создание своего апплета

Что такой апплет?

Апплет представляет из себя набор файлов, которые описывают процесс установки и запуска того или иного приложения на Microsoft RDS через RemoteApp. Это нужно для того, чтобы JumpServer мог запускать сессию доступа к этому приложению, автоматически авторизуясь в нем и скрывая параметры авторизации от пользователя.

Структура апплета

Каждый апплет обязательно состоит из этих файлов:

AppletName
  ├── i18n.yml
  ├── icon.png
  ├── main.py
  ├── manifest.yml
  └── setup.yml

main.py - скрипт запуска и авторизации в приложении

icon.png - значок апплета

manifest.yml - метаданные, то есть описание апплета

setup.yml - файл с описанием установки

i18n.yml - файл перевода на различные языки

Файл manifest.yml

Пример на базе апплета MySQL Workbench

В файле manifest.yml находится общая информация об апплете и указание его типа и протокола.

# (обязательно)
name: mysql_workbench8
display_name: "{{ 'MySQL Workbench' | trans }}"
comment: "{{ 'A tool for working with MySQL, to execute SQL and design tables' | trans }}"
# (обязательно))
version: 0.1.1
# (обязательно)
exec_type: python
# (обязательно)
author: Eric
# general или web (обязательно)
type: general
update_policy: always
edition: community
# (обязательно)
tags:
  - database
# (обязательно)
protocols:
  - mysqlworkbench

# перевод на дрругие языки
i18n:
  MySQL Workbench:
    en: MySQL Workbench
    zh: MySQL Workbench
    ja: MySQL Workbench

  A tool for working with MySQL, to execute SQL and design tables:
    en: A tool for working with MySQL, to execute SQL and design tables
    zh: 用于与MySQL一起工作的工具,用于执行SQL和设计表
    ja: MySQLでSQLを実行し、テーブルを設計するためのツール

Файл setup.yml

Файл setup.yml описывает параметры установки апплета на RDS сервер.

# тип установки ПО - msi,exe, zip, manual
type: msi
# адрес для загрузки дистрибутива ПО или имя файла, если дистрибутив вместе с апплетом в архиве
source: mysql-workbench-community-8.0.31-winx64.msi
# аргументы запуска установки
arguments:
  - /qn
  - /norestart
# директораия для установки
destination: C:\Program Files\MySQL\MySQL Workbench 8.0 CE
# путь и имя исполняемого файла
program: C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe
md5: d628190252133c06dad399657666974a
Скрипт main.py

main.py - основной скрипт апплета

Запуск приложения осуществляется вызовом команды:

python main.py base64_json_data

То есть запускается скрипт main.py и в него передаются параметры запуска, структура base64_json_data выглядит примерно так:

{
  "app_name": "mysql_workbench8",
  "protocol": "mysql",
  "user": {
    "id": "2647CA35-5CAD-4DDF-8A88-6BD88F39BB30",
    "name": "Administrator",
    "username": "admin"
  },
  "asset": {
    "asset_id": "46EE5F50-F1C1-468C-97EE-560E3436754C",
    "asset_name": "test_mysql",
    "address": "192.168.1.1",
    "protocols": [
      {
        "id": 2,
        "name": "mysql",
        "port": 3306
      }
    ]
  },
  "account": {
    "account_id": "9D5585DE-5132-458C-AABE-89A83C112A83",
    "username": "root",
    "secret": "test"
  },
  "platform": {
    "charset": "UTF-8"
  }
}
Содержимое main.py
import sys

from common import (block_input, unblock_input)  # Импорт функций для блокировки/разблокировки ввода
from common import convert_base64_to_dict  # Импорт функции для конвертации Base64 строки в словарь(массив)
from app import AppletApplication  # Импорт основного приложения

def main():
    base64_str = sys.argv[1]  # Получаем строку Base64 из аргументов командной строки
    data = convert_base64_to_dict(base64_str)  # Конвертируем Base64 строку в словарь
    # словарь data содержит все параметры запуска приложения: учетную запись, имя сервера, имя базы данных и тд в зависимости от типа приложения
    applet_app = AppletApplication(**data)  # Передаем данные словаря в функцию запуска приложения
    block_input()  # Блокируем ввод пользователя
    applet_app.run()  # Запускаем приложение
    unblock_input()  # Разблокируем ввод пользователя
    applet_app.wait()  # Ожидаем завершения работы приложения

if __name__ == '__main__':
    try:
        main()  # Запускаем основную функцию
    except Exception as e:
        print(e)  # Выводим ошибку, если она возникла

Содержимое app.py

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

import sys  # Импортирует модуль sys для работы с системными функциями

if sys.platform == 'win32':  # Проверяет, если операционная система — Windows
    from pywinauto import Application  # Импортирует библиотеку для автоматизации оконных приложений на Windows
    from pywinauto.controls.uia_controls import (ButtonWrapper, EditWrapper, MenuItemWrapper,
                                                 MenuWrapper, ComboBoxWrapper, ToolbarWrapper)
    # Импортирует различные элементы управления для взаимодействия с интерфейсом приложения

from common import (BaseApplication, wait_pid, )  # Импортирует базовое приложение и функцию ожидания процесса

_default_path = r"C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe"
# Определяет путь к приложению MySQL Workbench по умолчанию

class AppletApplication(BaseApplication):  # Определяет класс приложения, наследующий от BaseApplication

    def __init__(self, *args, **kwargs):  # Инициализирует приложение
        super().__init__(*args, **kwargs)  # Вызов конструктора родительского класса

        self.path = _default_path  # Устанавливает путь к приложению
        self.username = self.account.username  # Получает имя пользователя из учетной записи
        self.password = self.account.secret  # Получает пароль из учетной записи
        self.host = self.asset.address  # Получает адрес хоста из параметров актива
        self.port = self.asset.get_protocol_port(self.protocol)  # Получает порт по протоколу
        self.db = self.asset.spec_info.db_name  # Получает название базы данных
        self.pid = None  # Переменная для хранения ID процесса приложения
        self.app = None  # Переменная для хранения объекта приложения

    def run(self):  # Метод для запуска приложения
        app = Application(backend='uia')  # Создает объект приложения для взаимодействия через UI Automation
        app.start(self.path)  # Запускает приложение по указанному пути
        self.pid = app.process  # Сохраняет ID процесса приложения
        if not all([self.username, self.password, self.host]):  # Проверяет, заполнены ли необходимые параметры
            print(f'缺少必要的参数')  # Выводит сообщение об ошибке на китайском ("Не хватает обязательных параметров")
            return

        # Доступ к главному окну MySQL Workbench и меню "Database"
        menubar = app.window(title="MySQL Workbench", auto_id="MainForm", control_type="Window") \
            .child_window(title="Database", control_type="MenuItem")
        menubar.wait('ready', timeout=10, retry_interval=5)  # Ожидает готовности меню
        MenuItemWrapper(menubar.element_info).select()  # Открывает меню "Database"

        # Выбирает пункт меню "Connect to Database"
        cdb = menubar.child_window(title="Connect to Database", control_type="MenuItem")
        cdb.wait('ready', timeout=10, retry_interval=5)  # Ожидает готовности элемента
        MenuItemWrapper(cdb.element_info).click_input()  # Нажимает на элемент "Connect to Database"

        # Вводит хост
        host_ele = app.top_window().child_window(title="Host Name", auto_id="Host Name", control_type="Edit")
        EditWrapper(host_ele.element_info).set_edit_text(self.host)  # Вводит значение хоста

        # Вводит порт
        port_ele = app.top_window().child_window(title="Port", auto_id="Port", control_type="Edit")
        EditWrapper(port_ele.element_info).set_edit_text(self.port)  # Вводит значение порта

        # Вводит имя пользователя
        user_ele = app.top_window().child_window(title="User Name", auto_id="User Name", control_type="Edit")
        EditWrapper(user_ele.element_info).set_edit_text(self.username)  # Вводит имя пользователя

        # Вводит имя базы данных
        db_ele = app.top_window().child_window(title="Default Schema", auto_id="Default Schema", control_type="Edit")
        EditWrapper(db_ele.element_info).set_edit_text(self.db)  # Вводит имя базы данных

        # Нажимает на кнопку "OK" для подтверждения соединения
        ok_ele = app.top_window().child_window(title="Connection", auto_id="Connection", control_type="Window") \
            .child_window(title="OK", control_type="Button")
        ButtonWrapper(ok_ele.element_info).click()  # Нажимает на кнопку "OK"

        # Вводит пароль
        password_ele = app.top_window().child_window(title="Password", auto_id="Password", control_type="Edit")
        password_ele.wait('ready', timeout=10, retry_interval=5)  # Ожидает готовности поля для ввода пароля
        EditWrapper(password_ele.element_info).set_edit_text(self.password)  # Вводит пароль

        # Нажимает на кнопку "OK" для завершения подключения
        ok_ele = app.top_window().child_window(title="Button Bar", auto_id="Button Bar", control_type="Pane") \
            .child_window(title="OK", control_type="Button")
        ButtonWrapper(ok_ele.element_info).click()  # Нажимает на кнопку "OK"
        
        self.app = app  # Сохраняет объект приложения для дальнейшего использования

    def wait(self):  # Метод для ожидания завершения работы приложения
        wait_pid(self.pid)  # Ожидает завершения процесса с определенным ID (pid)