Перейти к содержимому

Настольные приложения

Эта страница переведена с английского. Читать страницу на английском

Приложение Zeyn можно поставлять как один исполняемый файл для рабочего стола. Крейт zeyn-desktop запускает собственный сервер приложения внутри процесса и показывает его в нативном окне через системный WebView:

ПлатформаWebViewСтатус
Windows 10 и 11WebView2 (Edge)собрано и проверено вручную
macOSWKWebViewкомпилируется через wry, вручную не проверялось
LinuxWebKitGTK 4.1компилируется через wry, если установлены libwebkit2gtk-4.1-dev и libgtk-3-dev, вручную не проверялось

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

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

Подключите крейт за feature, чтобы обычная серверная сборка не компилировала WebView:

[features]
desktop = ["dep:zeyn-desktop"]

[dependencies]
zeyn-desktop = { version = "0.0.2", optional = true }

Запускайте окно вместо App::serve:

#[cfg(feature = "desktop")]
fn main() -> std::io::Result<()> {
    zeyn_desktop::Desktop::new(app()?)
        .id("my-app")
        .title("My App")
        .size(1280, 800)
        .run()
}
  • id задаёт имя пользовательской папки данных и обеспечивает один экземпляр на каждый id. По умолчанию это имя исполняемого файла.
  • title показывается, пока страница не задаст document.title; после этого заголовок окна следует за страницей.
  • size это начальный размер в логических пикселях. Последующие запуски используют последний размер и положение.

run блокируется до закрытия окна, останавливает сервер и завершает процесс. С ZEYN_DESCRIBE=1 он выводит описание приложения и возвращается, поэтому zeyn check и zeyn routes продолжают работать.

Чтобы скрыть окно консоли в релизной сборке на Windows, поместите это в начало main.rs:

#![cfg_attr(all(feature = "desktop", not(debug_assertions)), windows_subsystem = "windows")]

examples/adisyon это полный пример, приложение кассы (point of sale):

cargo run -p zeyn-example-adisyon --features desktop

Сборка исполняемого файла

cd my-app
zeyn build-desktop                    # same as --features desktop
zeyn build-desktop --package my-app --features desktop,postgres

Команда запускает cargo build --release с оптимизацией на этапе компоновки, одним codegen unit и вырезанными символами, затем выводит путь и размер каждого исполняемого файла. Скорость остаётся на opt-level = 3.

Значок, сведения о версии и манифест (Windows)

Windows читает их из ресурсов внутри exe. Добавьте скрипт сборки, который встраивает их только для настольной сборки:

[features]
desktop = ["dep:zeyn-desktop", "dep:winresource"]

[build-dependencies]
winresource = { version = "0.1", optional = true, default-features = false }

build.rs:

fn main() {
    #[cfg(feature = "desktop")]
    if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
        let mut res = winresource::WindowsResource::new();
        res.set_icon("assets/app.ico")
            .set("ProductName", "My App")
            .set("FileDescription", "My App")
            .set_manifest_file("assets/app.exe.manifest");
        res.compile().unwrap();
    }
}

У значка должен быть идентификатор ресурса 1 (значение по умолчанию у set_icon); его используют окно и панель задач. Версия берётся из package.version. Манифест с поддержкой DPI для каждого монитора, UTF-8 и common controls 6 смотрите в examples/adisyon/build.rs и examples/adisyon/assets. Набор инструментов MSVC находит rc.exe в Windows SDK; набор GNU использует windres.

Рантайм WebView2

В Windows 11 и актуальной Windows 10 рантайм WebView2 предустановлен и обновляется сам (evergreen runtime). Exe его не включает, что сохраняет малый размер.

Для старых или закрытых машин:

  • Поставляйте вместе с установщиком evergreen bootstrapper от Microsoft (MicrosoftEdgeWebview2Setup.exe, около 2 МБ) и запускайте его с /silent /install, если рантайма нет. Он загружает рантайм один раз.
  • Для машин без интернета используйте автономный установщик evergreen (большой, офлайн) или runtime фиксированной версии, размещённый рядом с exe. Фиксированная версия сама не обновляется, поэтому обновления безопасности остаются на вас.

Без рантайма окно не может открыться, и run возвращает ошибку.

Модель безопасности

Сервер доступен только собственному окну приложения.

  • Только loopback. Слушатель привязывается к 127.0.0.1 на порту, который выбирает система. Он никогда не привязывается к 0.0.0.0 или адресу локальной сети.
  • Токен запуска. Каждый запуск создаёт случайный 256-битный токен. Окно сначала открывает /_desktop/start?t=<token>, который устанавливает токен как cookie HttpOnly, SameSite=Strict и перенаправляет на /. Каждый другой запрос должен нести токен в этом cookie или в заголовке x-zeyn-desktop, иначе получает 403. Статические ресурсы тоже защищены. Токен живёт только в памяти и сравнивается за постоянное время.
  • Host и Origin. Заголовок Host должен быть ровно 127.0.0.1:<port>, что останавливает DNS rebinding (сайт, имя которого разрешается в 127.0.0.1, отправляет в качестве host собственное имя). Заголовок Origin, если он есть, должен быть ровно http://127.0.0.1:<port>. Оба условия действуют для каждого пути, включая конечные точки start и focus.
  • Другие локальные программы могут подключиться к порту, но не знают токена. Другие учётные записи пользователей не могут прочитать файл экземпляра в пользовательской папке данных.
  • Навигация. Окно показывает только собственный origin, свои URL blob: и about:blank. Ссылки http, https и mailto открываются обработчиком системы по умолчанию; любая другая схема (file:, пользовательские обработчики протоколов, javascript:) отбрасывается. window.open никогда не создаёт второй WebView.
  • Devtools включены в отладочных сборках и выключены в релизных.
  • Загрузки разрешены только с origin приложения и попадают в папку «Загрузки» пользователя, никогда не перезаписывая файл (orders.csv, orders (1).csv, ...). Несколько загрузок подряд разрешены без запроса. Прочие запросы разрешений (камера, микрофон, буфер обмена) получают обычный запрос WebView.

Что защита не покрывает:

  • Программа, запущенная от того же пользователя, может читать память процесса или подключить отладчик; ни одно локальное приложение не может от этого защититься.
  • WebView2 учитывает переменную окружения WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS, поэтому тот, кто может задавать переменные окружения пользователя, может включить удалённую отладку. Тот же пользователь, тот же предел, что выше.
  • Собственная аутентификация приложения по-прежнему действует внутри окна. DevAuth в настольной сборке даёт полный доступ любому, кто сидит за машиной.

Поведение

  • Один экземпляр. Первый запуск записывает instance (порт и отдельный ключ фокуса) в папку данных. Второй запуск отправляет ключ на /_desktop/focus; первое окно выходит на передний план, а второй процесс завершается примерно за 150 мс. Если два запуска стартуют в один и тот же момент, могут открыться оба.
  • Состояние окна. Размер, положение и признак развёрнутости сохраняются в window.json при закрытии. Сохранённое положение, которого больше нет ни на одном мониторе, игнорируется.
  • Завершение. Закрытие окна освобождает WebView, корректно останавливает сервер (не более 2 секунд) и завершает процесс. Процессы WebView завершаются вместе с ним.
  • Папка данных. %LOCALAPPDATA%\<id> в Windows, ~/Library/Application Support/<id> в macOS, ~/.local/share/<id> в Linux. Профиль WebView (кэш, локальное хранилище) лежит в webview внутри неё, поэтому exe работает и из папки только для чтения, например Program Files.
  • Без значка в трее. Не встроен.

Размер и память

Измерено в Windows 11 на examples/adisyon, релизная сборка, рантайм WebView2 156:

exe от zeyn build-desktop6.72 МБ (7.25 МБ с обычным релизным профилем)
Процесс приложения, простой4.3 МБ private, 25.6 МБ working set
Процесс приложения и процессы WebView2, простой7 процессов, около 180 МБ private, 370 МБ working set
То же с --disable-gpuоколо 114 МБ private, 330 МБ working set
Второй запуск до завершенияоколо 130 мс

Доминирует WebView. Один только его GPU-процесс занимает около 88 МБ. Для простых экранов на машинах с малой памятью запускайте exe с WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--disable-gpu (например, из ярлыка или скрипта запуска), чтобы сэкономить около 65 МБ ценой программной отрисовки.