Подключение к приложениям Termit из стороннего клиента
Termit позволяет запускать сессии удалённого доступа в обход основного окна приложения. Благодаря этому возможно инициировать подключения из внешних систем и встраивать Termit в сторонние интерфейсы. Для интеграции необходимо выполнить следующие действия, описанные ниже.
Что такое IPC
Inter‑Process Communication (IPC) — механизм обмена данными между процессами. В Termit, десктоп-клиенте, IPC‑сервер предоставляет внешний интерфейс, через который другие приложения могут отправлять команды и получать ответы от десктоп‑клиента.
Транспорт и формат обмена
Формат запроса (Client → Server)
Каждый запрос клиента должен иметь следующую структуру:
{
"type": "request",
"operation": "<OPERATION_NAME>",
"data": { ... },
"requestId": "<string>"
}
Где:
-
type— строка, всегда должна иметь значение "request"; -
operation— имя вызываемой операции; -
data— объект с параметрами операции (структура зависит от операции); -
requestId— строковый идентификатор запроса, используется для сопоставления запроса и ответа.- Ошибки формата
-
При получении некорректного JSON или неверной структуре запроса сервер возвращает ответ:
BAD_REQUEST
Формат запроса (Server → Client)
Успешный ответ
{
"type": "response",
"requestId": "<same as in request>",
"success": true,
"data": <результат>
}
Ошибка
{
"type": "error",
"requestId": "<same as in request>",
"success": false,
"error": {
"code": "<ERROR_CODE>",
"message": "<message>"
}
}
- Коды ошибок
-
-
BAD_REQUEST— неверный формат запроса или структура JSON; -
OP_FAILED— ошибка выполнения операции; -
DUPLICATE_OPERATION— операция уже выполняется; -
DEADLOCK— ошибка очереди или зависание.
-
Очередь операций
Некоторые операции не могут выполняться параллельно.
Если в запросе указано enqueue: true, сервер помещает операцию в очередь ожидания.
Если операция с тем же ключом уже выполняется, а новая помечена как enqueue: false, сервер возвращает ошибку:
{
"type": "error",
"requestId": "<id>",
"success": false,
"error": {
"code": "DUPLICATE_OPERATION",
"message": "<operation> already running"
}
}
Поддерживаемые операции
-
CONNECT_BROKERЗапрос:
{ "type":"request", "operation":"CONNECT_BROKER", "data":{"broker":"host"}, "requestId":"1" }Поведение:
-
Проверяет текущее состояние подключения.
-
Если брокер не подключён — устанавливает соединение.
-
Если уже подключён к тому же адресу — возвращает ошибку.
Успех:
{ "type":"response", "requestId":"1", "success":true, "data":true }
-
-
DISCONNECT_BROKERВызывает
disconnectFromBroker() -
AUTH_USERПередаёт данные в
authService.authByCredentials()Ошибка:
OP_FAILED, "Auth failed" -
FETCH_RESOURCESВызывает
resourcesService.fetchFullResources(). Ответ: Возвращает список доступных ресурсов (структура ответа зависит от реализации). -
START_SESSION-
Загружает ресурс по идентификатору
resourceId; -
Запускает клиентскую сессию;
-
Открывает окно клиента.
-
-
LOGOUTВызывает
logoutUser() -
QUITПоведение:
-
Если
graceful = true— вызываетapp.quit()(корректное завершение). -
Если
graceful = false— вызываетapp.exit(0)(немедленное завершение).
-
| для данной операции коды ошибок не предусмотрены. |
Логика обработки входящих данных
-
Клиент отправляет одну или несколько строк JSON. Каждая строка должна завершаться символом
\n. -
Сервер буферизует полученные данные.
-
Сервер извлекает из буфера отдельные строки.
-
Каждая строка парсится как JSON:
-
Если парсинг завершился ошибкой, сервер возвращает ответ:
BAD_REQUEST.
-
-
Проверяет структуру запроса. Если операция существует:
-
при
enqueue = false— операция выполняется синхронно (немедленно). -
при
enqueue = true— операция ставится в очередь и выполняется асинхронно.
-
-
После завершении выполнения операции сервер отправляет ответ клиенту.
Операции, использующие очередь
Следующие операции выполняются последовательно через очередь (не параллельно):
-
CONNECT_BROKER -
DISCONNECT_BROKER -
AUTH_USER -
START_SESSION -
LOGOUT -
QUIT
Операция`FETCH_RESOURCES` — выполняется немедленно, минуя очередь.
Пример успешного диалога
Запрос клиента (Client →):
{"type":"request","operation":"CONNECT_BROKER","data":{"broker":"b.local"},"requestId":"1"}
Ответ сервера (Server →):
{"type":"response","requestId":"1","success":true,"data":true}
Запрос клиента (Client →):
{"type":"request","operation":"FETCH_RESOURCES","data":{},"requestId":"2"}
Ответ сервера (Server →):
{"type":"response","requestId":"2","success":true,"data":[ ... ]}
Пример диалога с ошибкой
Запрос клиента (Client →):
{"type":"request","operation":"CONNECT_BROKER","data":{"broker":"b.local"},"requestId":"1"}
Ответ сервера (Server →):
{"type":"response","requestId":"1","success":true,"data":true}
Запрос клиента (Client →):
{"type":"request","operation":"CONNECT_BROKER","data":{"broker":"b.local"},"requestId":"2"}
Ответ сервера (Server →):
{
"type":"error",
"requestId":"2",
"success":false,
"error":{"code":"OP_FAILED","message":"broker already connected"}
}
Пример клиентского скрипта и предварительные действия
Предварительные действия
-
Установите десктоп-клиент на машину, где будет выполняться IPC-взаимодействие.
-
Запустите клиент в headless-режиме с помощью команды:
termit-desktop.exe --headless=trueВ этом режиме клиент работает без UI, но не поднимается IPC-сервер:
-
Windows:
\\.\pipe\termit-desktop; -
Linux/macOS:
/tmp/termit-desktop.sock.
-
-
В брокере
BROKER_FQDNскрипт поднимает сессию с ресурсомRESOURCE_NAMEпод пользователемUSERNAMEс паролемPASSWORD. Для этого в файлеipc.pyукажите следующие параметры:BROKER_FQDN = "<адрес брокера>" USERNAME = "<имя пользователя>" PASSWORD = "<пароль>" RESOURCE_NAME = "<имя ресурса>"Пример скрипта
import os import json import time import socket BROKER_FQDN = "<broker_fqdn" USERNAME = "<username>" PASSWORD = "<password>" RESOURCE_NAME = "<app_name>" def wait(ms): time.sleep(ms / 1000) def send(write_fn, data): line = json.dumps(data) + "\n" write_fn(line.encode("utf-8")) print("[CLIENT] connecting...") # ------------------------- # WINDOWS (Named Pipe) # ------------------------- if os.name == "nt": PIPE = r"\\.\pipe\termit-desktop" flags = os.O_RDWR if hasattr(os, "O_BINARY"): flags |= os.O_BINARY pipe = os.open(PIPE, flags) def write_fn(data): os.write(pipe, data) def read_fn(): return os.read(pipe, 4096) print("[CLIENT] connected via Windows named pipe") # ------------------------- # LINUX / UNIX (UNIX socket) # ------------------------- else: PIPE = "/tmp/termit-desktop.sock" sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(PIPE) def write_fn(data): sock.sendall(data) def read_fn(): return sock.recv(4096) print("[CLIENT] connected via UNIX socket") # ------------------------- # SEND INITIAL REQUESTS # ------------------------- send(write_fn, { "type": "request", "operation": "CONNECT_BROKER", "data": {"broker": BROKER_FQDN}, "requestId": "1" }) wait(2000) send(write_fn, { "type": "request", "operation": "AUTH_USER", "data": {"username": USERNAME, "password": PASSWORD}, "requestId": "3" }) wait(3000) send(write_fn, { "type": "request", "operation": "FETCH_RESOURCES", "data": {}, "requestId": "8" }) wait(3000) # ------------------------- # MAIN LOOP # ------------------------- buffer = b"" while True: chunk = read_fn() if not chunk: break buffer += chunk while b"\n" in buffer: raw, buffer = buffer.split(b"\n", 1) if not raw.strip(): continue try: json_msg = json.loads(raw.decode("utf-8")) except Exception: print("[CLIENT] parse error:", raw) continue print("[CLIENT] server replied:", json_msg) # ------------------------- # HANDLE "broker already connected" # ------------------------- if ( not json_msg.get("success") and json_msg.get("error", {}).get("message") == "broker already connected" ): send(write_fn, { "type": "request", "operation": "DISCONNECT_BROKER", "data": {}, "requestId": "5" }) wait(500) send(write_fn, { "type": "request", "operation": "CONNECT_BROKER", "data": {"broker": BROKER_FQDN}, "requestId": "6" }) wait(2000) send(write_fn, { "type": "request", "operation": "AUTH_USER", "data": {"username": USERNAME, "password": PASSWORD}, "requestId": "3" }) wait(3000) # ------------------------- # HANDLE FETCH_RESOURCES # ------------------------- if json_msg.get("success") and json_msg.get("requestId") == "8": resources = json_msg.get("response", {}).get("resources", []) resource_id = None for resource in resources: if resource.get("name") == RESOURCE_NAME: resource_id = resource.get("id") break if resource_id: print(f"[CLIENT] using resource '{RESOURCE_NAME}' with ID: {resource_id}") send(write_fn, { "type": "request", "operation": "START_SESSION", "data": {"resourceId": resource_id}, "requestId": "4" }) else: print(f"[CLIENT] resource '{RESOURCE_NAME}' not found")