Русский
Русский
English
Статистика
Реклама

Ipc

Как nix-сигналы позволяют читать память других процессов

28.11.2020 16:23:21 | Автор: admin
Есть такая очень старая и вросшая в *nix с корнями штука под названием сигналы. Идея этих примитивов очень проста: реализовать программный аналог прерываний. Различные процессы могут посылать сигналы друг другу и самим себе, зная process id (pid) получателя. Процесс-получатель волен либо назначить функцию-обработчик сигнала, которая будет автоматически вызываться при его получении, либо игнорировать его с помощью специальной маски, либо же довериться поведению по умолчанию. So far so good.

Поведение по умолчанию при получении сигнала А что означают эти успокаивающие слова? Уверен, не то, что вы ожидали. Вики говорит, что обработчики 28 стандартных сигналов (существуют и другие!) по умолчанию таковы: 2 игнорируются, 4 вызывают остановку процесса, 1 его продолжение, 11 его завершение, 10 его завершение с созданием дампа памяти. Вот это уже интересно! Итак, дело обстоит следующим образом: даже если ваша программа никак не упоминает сигналы в исходном коде, на самом деле она их использует, причём весьма драматичным образом.

С этого момента нам придётся копнуть поглубже. Кто и кому может посылать сигналы? Вики говорит: Процесс (или пользователь из оболочки) с эффективным UID, не равным 0 (UID суперпользователя), может посылать сигналы только процессам с тем же UID. Итак, если вы запускаете 100 программ, то любая из них может запросто убить все эти 100 программ с помощью системного API, даже если все программы (кроме убийцы) никак не упоминали сигналы в своём исходном коде. Если же вы работали под учёткой root-а, то вообще не важно, кто запустил те или иные процессы, всё равно их можно запросто завершить. Узнать pid конкретного процесса и выполнить его заказное убийство, разумеется, можно, но ещё проще убить всех кого можно путём простого перебора pid-ов.

Погоди-погоди, не гони лошадей. Ты упоминал, что сигналы можно обрабатывать и игнорировать! слышу я голос своего читателя. Что скажет Вики? Для альтернативной обработки всех сигналов, за исключением SIGKILL и SIGSTOP, процесс может назначить свой обработчик или игнорировать их возникновение модификацией своей сигнальной маски. Смотрим на действия по умолчанию при получении этих сигналов и видим: Завершение процесса, Остановка процесса. Получается, что эти два действия мы можем сделать всегда, когда посылка сигналов SIGKILL и SIGSTOP жертве в принципе возможна. Единственное исключение процесс с pid 1 (init), который имеет право игнорировать или обрабатывать любые сигналы, включая KILL и STOP. Возможно, мы даже из-под root-а не сможем убить один из главнейших системных процессов, но по-хорошему это требует дополнительного исследования.

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

Абстрактные рассуждения это очень круто, но давай-ка ближе к конкретике, скажет мне требовательный читатель. Окей, нет проблем! Любому пользователю *nix хорошо знакома такая программа, как bash. Эта программа развивается уже почти 30 лет и обладает целой горой возможностей. Завалим-ка её для наглядности и получим из её памяти какую-нибудь вкуснятину!

Я достаю из широких штанин свою домашнюю Ubuntu 16.04.2 и запускаю на ней две копии bash 4.3.46. В одной из них я выполню гипотетическую команду с секретными данными: export password=SECRET. Давайте на время забудем про файл с историей команд, в которую тоже записался бы пароль. Наберём в этом же окне команду ps, чтобы узнать pid этого процесса скажем, 3580.

Не закрывая первое окно, перейдём во второе. Команда ps в нём даст другой pid этого экземпляра bash скажем, 5378. Чисто для наглядности именно из этого второго bash-а отправим сигнал первому командой kill -SIGFPE 3580. Да, уважаемый читатель, это полный абсурд: процесс 2 говорит никак не связанному с ним процессу 1, что в этом самом процессе 1 произошла ошибочная арифметическая операция. На экране появляется такое вот окошко:



Произошло желанное аварийное завершение с созданием дампа памяти, то есть bash похоже не обрабатывает и не игнорирует этот сигнал. Загуглив, где мне искать дамп, я нашёл развёрнутый ответ (раз, два). В моей Убунте дело обстоит так: если приложение из стандартного пакета падает из-за сигнала, отличного от SIGABRT, то дамп передаются программе Apport. Это как раз наш случай! Данная программа компонует файл с диагностической информацией и выдаёт окошко, показанное выше. Официальный сайт гордо заявляет: Apport collects potentially sensitive data, such as core dumps, stack traces, and log files. They can contain passwords, credit card numbers, serial numbers, and other private material. Так-так, интересно, а где там у нас лежит этот файл? Ага, /var/crash/_bin_bash.1000.crash. Вытащим его содержимое в папку somedir: apport-unpack /var/crash/_bin_bash.1000.crash somedir. Помимо разных неинтересных мелочей там будет вожделенный дамп памяти под названием CoreDump.

Вот он, момент истины! Давайте поищем в этом файле строку password и посмотрим, что интересного мы получим в ответ. Команда strings CoreDump | grep password напомнит забывчивому хакеру, что password есть SECRET. Чудесно!

То же самое я проделал и со своим любимым текстовым редактором gedit, начав набирать текст в буфере, а затем считав его уже из дампа. Никаких проблем! В этот момент Вики предостерегающе шепнула на ухо: Иногда (например, для программ, выполняемых от имени суперпользователя) дамп памяти не создаётся из соображений безопасности. Тааак, проверим При получении сигнала от рутового bash-а второй рутовый bash упал с созданием дампа памяти, но из-за прав доступа (-rw-r----- с владельцем root) прочитать его уже не так просто, как прежние, владельцем которых был мой пользовательский аккаунт. Что ж, коли гипотетической программе-киллеру удалось послать сигнал с UID суперпользователя, то и такой дамп она сможет потрогать.

Дотошный читатель может заметить: Тебе было очень легко найти нужные данные в море мусора. Чистая правда, но я уверен: если вы знаете, какую рыбу вы хотите поймать и где она плавает, то найти её в сетях дампа должно быть реально почти всегда. Скажем, никто не мешает скачать пакет с отладочной информацией для упавшей программы и узнать содержимое интересующих вас переменных в GDB путём post-mortem отладки.

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

Может возникнуть возражение: зловредина может просто воспользоваться средствами отладки для утягивания интересных данных из приложения. Это действительно так. К чему же в таком случае этот пост? Моя мысль такова: первое, что приходит на ум при попытке пресечь кражу данных из приложений это как раз ограничения на отладочные инструменты. Наверняка антивирусные инструменты отлавливают использование ptrace() в первую очередь это очень подозрительное событие. Сигналы же совсем другое дело. Один процесс посылает другому стандартный сигнал ну и что? На первый взгляд, это совершенно нормальное событие. Но, как мы уже видели, это может привести к аварийному завершению приложения, созданию дампа ядра в какой-то папке, из которой его можно будет попробовать утянуть.

Когда я попытался открыть страничку авторизации vk.com и свалить Firefox тем же роковым сигналом, он упал, но вызвал свой обработчик дампов. Дампы в хитром формате minidump сохраняются по адресу ~/.mozilla/firefox/Crash Reports/{pending или submitted} и требуют дополнительного исследования. Вот что вы узнаете, если в окошке настроек кликните на Learn more напротив галочки (текст ниже раньше висел по адресу www.mozilla.org/ru/privacy/firefox/#crash-reporter):



При желании вы можете отправить сообщение об ошибке в корпорацию Mozilla после падения браузера Firefox. Такое сообщение содержит технические данные, которые мы используем для улучшения работы Firefox, в том числе информацию о причине падения, об активном URL-адресе на момент падения, а также о состоянии памяти компьютера на момент падения. Сообщения об ошибках, которые мы получаем, могут содержать персональную информацию. Некоторые части сообщений об ошибках мы публикуем в открытом доступе по адресу crash-stats.mozilla.com. Перед публикацией сообщений об ошибках мы принимаем меры для автоматического удаления персональной информации. Мы не удаляем ничего из написанного вами в полях для комментариев. В URL-ках редко бывает что-то по-настоящему вкусное, а вот есть ли в дампах пароли или cookie, вопрос хороший!

На этой таинственной и интригующей ноте я закончу. Ко мне пришёл сигнал, который я забыл явно обработать.

P. S. Я написал простую программу с таким обработчиком сигнала SIGUSR1: напечатать на экран строку 1, войти в бесконечный цикл. Я надеялся, что если много раз посылать этой программе сигнал SIGUSR1, то обработчик будет вызываться многократно, что вызовет переполнение стека. К моему сожалению, обработчик вызывался лишь один раз. Окей, напишем аналогичный обработчик сигнала SIGUSR2 и будем посылать два разных сигнала в надежде, что это свалит жертву Увы, но и это не помогло: каждый из обработчиков был вызван лишь однажды. Переполняли-переполняли, да не выпереполняли!

P. S. 2. В мире Windows есть некое подобие сигналов сообщения, которые можно отправлять окнам. Весьма вероятно, что их тоже можно использовать for fun and profit!

Оригинал опубликован в моём блоге 5.05.17.
Подробнее..

ReactRedoor IPC мониторинг

12.05.2021 18:04:50 | Автор: admin

В одном из наших проектов, мы использовали IPC (inter-process communication) на сокетах. Довольно большой проект, торгового бота, где были множество модулей которые взаимодействовали друг с другом. По мере роста сложности стал вопрос о мониторинге, что происходит в микросервисах. Мы решили создать свое приложение для отслеживания, потока данных на всего двух библиотеках react и redoor. Я хотел бы поделиться с вами нашим подходом.

Микросервисы обмениваются между собой JSON объектами, с двумя полями: имя и данные. Имя - это идентификатор какому сервису предназначается объект и поле данные - полезная нагрузка. Пример:

{ name:'ticket_delete', data:{id:1} }

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

И так начнем.Для примера сделаем простейшее приложение и веб сервер. Приложение будет состоять из трех модулей. На картинке они обозначены пунктирными линиями. Таймер, статистика и кнопки управления статистикой.

Создадим простой Web Socket сервер.

/** src/ws_server/echo_server.js */const WebSocket = require('ws');const wss = new WebSocket.Server({ port: 8888 });function sendToAll( data) {  let str = JSON.stringify(data);  wss.clients.forEach(function each(client) {    client.send(str);  });}// Отправляем данные каждую секундуsetInterval(e=>{  let d = new Date();  let H = d.getHours();  let m = ('0'+d.getMinutes()).substr(-2);  let s = ('0'+d.getSeconds()).substr(-2);  let time_str = `${H}:${m}:${s}`;  sendToAll({name:'timer', data:{time_str}});},1000);

Сервер каждую секунду формирует строку с датой и отправляет всем подключившимся клиентам. Открываем консоль и запускаем сервер:

node src/ws_server/echo_server.js

Теперь перейдем к проекту приложения. Для сборки и отладки будем использовать rollup конфигурация ниже.

rollup.config.js
import serve from 'rollup-plugin-serve';import babel from '@rollup/plugin-babel';import { nodeResolve } from '@rollup/plugin-node-resolve';import commonjs from '@rollup/plugin-commonjs';import hmr from 'rollup-plugin-hot'import postcss from 'rollup-plugin-postcss';import autoprefixer from 'autoprefixer'import replace from '@rollup/plugin-replace';const browsers = [  "last 2 years",  "> 0.1%",  "not dead"]let is_production = process.env.BUILD === 'production';const replace_cfg = {  'process.env.NODE_ENV': JSON.stringify( is_production ? 'production' : 'development' ),  preventAssignment:false,}const babel_cfg = {    babelrc: false,    presets: [      [        "@babel/preset-env",        {          targets: {            browsers: browsers          },        }      ],      "@babel/preset-react"    ],    exclude: 'node_modules/**',    plugins: [      "@babel/plugin-proposal-class-properties",      ["@babel/plugin-transform-runtime", {         "regenerator": true      }],      [ "transform-react-jsx" ]    ],    babelHelpers: 'runtime'}const cfg = {  input: [    'src/main.js',  ],  output: {    dir:'dist',    format: 'iife',    sourcemap: true,    exports: 'named',  },  inlineDynamicImports: true,  plugins: [    replace(replace_cfg),    babel(babel_cfg),    postcss({      plugins: [        autoprefixer({          overrideBrowserslist: browsers        }),      ]    }),    commonjs({        sourceMap: true,    }),    nodeResolve({        browser: true,        jsnext: true,        module: false,    }),    serve({      open: false,      host: 'localhost',      port: 3000,    }),  ],} ;export default cfg;

Точка входа нашего проекта main.js создадим его.

/** src/main.js */import React, { createElement, Component, createContext } from 'react';import ReactDOM from 'react-dom';import {Connect, Provider} from './store'import Timer from './Timer/Timer'const Main = () => (  <Provider>    <h1>ws stats</h1>    <Timer/>  </Provider>);const root = document.body.appendChild(document.createElement("DIV"));ReactDOM.render(<Main />, root);

Теперь создадим стор для нашего проекта

/** src/store.js */import React, { createElement, Component, createContext } from 'react';import createStoreFactory from 'redoor';import * as actionsWS from './actionsWS'import * as actionsTimer from './Timer/actionsTimer'const createStore = createStoreFactory({Component, createContext, createElement});const { Provider, Connect } = createStore(  [    actionsWS,     // websocket actions    actionsTimer,  // Timer actions  ]);export { Provider, Connect };

Прежде чем создавать модуль таймера нам надо получать данные от сервера. Создадим акшнес файл для работы с сокетом.

/** src/actionsWS.js */export const  __module_name = 'actionsWS'let __emit;// получаем функцию emit от redoorexport const bindStateMethods = (getState, setState, emit) => {  __emit = emit};// подключаемся к серверуlet wss = new WebSocket('ws://localhost:8888')// получаем все сообщения от сервера и отправляем их в поток redoorwss.onmessage = (msg) => {  let d = JSON.parse(msg.data);  __emit(d.name, d.data);} 

Здесь надо остановиться поподробнее. Наши сервисы отправляют данные в виде объекта с полями: имя и данные. В библиотеке redoor можно так же создавать потоки событий в которые мы просто передаем данные и имя. Выглядит это примерно так:

   +------+    | emit | --- events --+--------------+----- ... ------+------------->+------+              |              |                |                      v              v                v                 +----------+   +----------+     +----------+                 | actions1 |   | actions2 | ... | actionsN |                 +----------+   +----------+     +----------+

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

Теперь создадим собственно сам модуль таймера. В папке Timer создадим два файла Timer.js и actionsTimer.js

/** src/Timer/Timer.js */import React from 'react';import {Connect} from '../store'import s from './Timer.module.css'const Timer = ({timer_str}) => <div className={s.root}>  {timer_str}</div>export default Connect(Timer);

Здесь все просто, таймер берет из глобального стейта timer_str который обновляется в actionsTimer.js. Функция Connect подключает модуль к redoor.

/** src/Timer/actionsTimer.js */export const  __module_name = 'actionsTimer'let __setState;// получаем метод для обновления стейтаexport const bindStateMethods = (getState, setState) => {  __setState = setState;};// инициализируем переменную таймераexport const initState = {  timer_str:''}// "слушаем" поток событий нам нужен "timer"export const listen = (name,data) =>{  name === 'timer' && updateTimer(data);}// обновляем стейт function updateTimer(data) {  __setState({timer_str:data.time_str})}

В акшес файле, мы "слушаем" событие timer таймера (функция listen) и как только оно будет получено обновляем стейт и выводим строку с данными.

Подробнее о функциях redoor:

__module_name - зарезервированная переменная нужна просто для отладки она сообщает в какой модуль входят акшенсы.

bindStateMethods - функция для получения setState, поскольку данные приходят асинхронно нам надо получить в локальных переменных функцию обновления стейта.

initState - функция или объект инициализации данных модуля в нашем случае это timer_str

listen- функция в которую приходят все события сгенерированные redoor.

Готово. Запускаем компиляцию и открываем браузер по адресу http://localhost:3000

npx rollup -c rollup.config.js --watch

Должны появиться часики с временем. Перейдём к более сложному. По аналогии с таймером добавим еще модуль статистики. Для начала добавим новый генератор данных в echo_server.js

/** src/ws_server/echo_server.js */...let g_interval = 1;// Данные статистикиsetInterval(e=>{  let stats_array = [];  for(let i=0;i<30;i++) {    stats_array.push((Math.random()*(i*g_interval))|0);  }  let data  = {    stats_array  }  sendToAll({name:'stats', data});},500);...

И добавим модуль в проект. Для этого создадим папку Stats в которой создадим Stats.js и actionsStats.js

/** src/Stats/Stats.js */import React from 'react';import {Connect} from '../store'import s from './Stats.module.css'const Bar = ({h})=><div className={s.bar} style={{height:`${h}`px}}>  {h}</div>const Stats = ({stats_array})=><div className={s.root}>  <div className={s.bars}>    {stats_array.map((it,v)=><Bar key={v} h={it} />)}  </div></div>export default Connect(Stats);
/** src/Stats/actionsStats.js */export const  __module_name = 'actionsStats'let __setState = null;export const bindStateMethods = (getState, setState, emit) => {  __setState = setState;}export const initState = {  stats_array:[],}export const listen = (name,data) =>{  name === 'stats' && updateStats(data);}function updateStats(data) {  __setState({    stats_array:data.stats_array,  })}

и подключаем новый модуль к стору

/** src/store.js */...import * as actionsStats from './Stats/actionsStats'const { Provider, Connect } = createStore(  [    actionsWS,    actionsTimer,    actionsStats //<-- модуль Stats  ]);...

В итоге мы должны получить это:

Как видите модуль Stats принципиально не отличается от модуля Timer, только отображение не строки, а массива данных. Что если мы хотим не только получать данные, но и отправлять их на сервер? Добавим управление статистикой.

В нашем примере переменная g_interval это угловой коэффициент наклона нормировки случайной величины. Попробуем ей управлять с нашего приложения.

Добавим пару кнопок к графику статистики. Плюс будет увеличвать значение interval минус уменьшать.

/** src/Stats/Stats.js */...import Buttons from './Buttons' // импортируем модуль...const Stats = ({cxRun, stats_array})=><div className={s.root}>  <div className={s.bars}>    {stats_array.map((it,v)=><Bar key={v} h={it} />)}  </div>  <Buttons/> {/*Модуль кнопочки*/}</div>...

И сам модуль с кнопочками

/** src/Stats/Buttons.js */import React from 'react';import {Connect} from '../store'import s from './Stats.module.css'const DATA_INTERVAL_PLUS = {  name:'change_interval',  interval:1}const DATA_INTERVAL_MINUS = {  name:'change_interval',  interval:-1}const Buttons = ({cxEmit, interval})=><div className={s.root}>  <div className={s.btns}>      <button onClick={e=>cxEmit('ws_send',DATA_INTERVAL_PLUS)}>        plus      </button>      <div className={s.len}>interval:{interval}</div>      <button onClick={e=>cxEmit('ws_send',DATA_INTERVAL_MINUS)}>        minus      </button>  </div></div>export default Connect(Buttons);

Получаем панель с кнопочками:

И модифицируем actionsWS.js

/** src/actionsWS.js */...let wss = new WebSocket('ws://localhost:8888')wss.onmessage = (msg) => {  let d = JSON.parse(msg.data);  __emit(d.name, d.data);}// "слушаем" событие отправить данные на серверexport const listen = (name,data) => {  name === 'ws_send' && sendMsg(data);}// отправляем данныеfunction sendMsg(msg) {  wss.send(JSON.stringify(msg))}

Здесь мы в модуле Buttons.js воспользовались встроенной функции (cxEmit) создания события в библиотеке redoor. Событие ws_send "слушает" модуль actionsWS.js. Полезная нагрузка data - это два объекта: DATA_INTERVAL_PLUS и DATA_INTERVAL_MINUS. Таким образам если нажать кнопку плюс на сервер будет отправлен объект { name:'change_interval', interval:1 }

На сервере добавляем

/** src/ws_server/echo_server.js */...wss.on('connection', function onConnect(ws) {  // "слушаем" приложение на событие "change_interval"  // от модуля Buttons.js  ws.on('message', function incoming(data) {    let d = JSON.parse(data);    d.name === 'change_interval' && change_interval(d);  });});let g_interval = 1;// меняем интервалfunction change_interval(data) {  g_interval += data.interval;  // создаем событие, что интервал изменен  sendToAll({name:'interval_changed', data:{interval:g_interval}});}...

И последний штрих необходимо отразить изменение интервала в модуле Buttons.js. Для этого в actionsStats.js начнём слушать событие "interval_changed" и обновлять переменную interval

/** src/Stats/actionsStats.js */...export const initState = {  stats_array:[],  interval:1 // добавляем переменную интервал}export const listen = (name,data) =>{  name === 'stats' && updateStats(data);    // "слушаем" событие обновления интервала  name === 'interval_changed' && updateInterval(data);}// обнавляем интервалfunction updateInterval(data) {  __setState({    interval:data.interval,  })}function updateStats(data) {  __setState({    stats_array:data.stats_array,  })}

Итак, мы получили три независимых модуля, где каждый модуль следит только за своим событием и отображает только его. Что довольно удобно когда еще не ясна до конца структура и протоколы на этапе прототипирования. Надо только добавить, что поскольку все события имеют сквозную структуру то надо четко придерживаться шаблона создания события мы для себя выбрали такую: (MODULEN AME)_(FUNCTION NAME)_(VAR NAME).

Надеюсь было полезно. Исходные коды проекта, как обычно, на гитхабе.

Подробнее..
Категории: Javascript , React , Node.js , Reactjs , Nodejs , Ipc , Webscoket

Окей Гугл, гайд по AIDL

23.01.2021 14:23:52 | Автор: admin

Вступление

Привет хабр!

Совсем недавно у меня появилась необходимость разработать функционал для взаимодействия между двумя приложениями одной компании, и недолго гуглив я открыл для себя мир межпроцессного взаимодействия в андройд - AIDL.

О существовании AIDL я знал довольно давно, однако до этого момента с IPC в андройде не сталкивался, и этот опыт был для меня первым.

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

Итак, приступим.

Как это работает на примере калькулятора

В основе взаимодействия лежит bound-сервис, предоставляющий сторонним приложениям доступ к определенному набору методов. Другими словами, одно приложение выступает в роли "клиента" а другое в роли "сервера", причем общаются они по строго заданному интерфейсу, который идентичен для обоих приложений.

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

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

Итак, имеем два модуля-приложения: :app-server и :app-client, из названия понятно кто за что отвечает.

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

Далее в приложении-сервере нам необходимо написать тот самый bound-сервис, через который и будет происходить общение между нашими приложениями. В единственном методе сервиса onBind пока-что будем возвращать null.

Теперь перейдем к самой работе с AIDL. В модуле :aidl в папке src/main/aidl/*пакет приложения* создадим файл Calculator.aidl, в котором напишем интерфейс имеющий всего один метод сложения чисел:

package com.example.aidl;// Calculator.aidlinterface Calculator {    int sum(int first, int second);}

Синтаксис в AIDL такой же как в Java, за исключением некоторых особенностей о которых мы поговорим по ходу статьи.

Однако первое что вы заметите при написании AIDL-интерфейса, так это его плохую поддержку в Android Studio - забудьте про автодополнение кода, автоматическую подмену импортов и пакета при перемещении файла, да черт, там даже подсветка для JavaDoc-комментариев отсутствует! Очень разочаровало.

Во время сборки проекта у нас запустится gradle-таск, который сгенерирует java-интерфейс содержащий в себе логику IPC транзакций. О его внутренностях нам знать не обязательно, нас интересует абстрактный класс Calculator.Stub который является наследником android.os.IBinder, его-то мы и должны будем вернуть в методе onBind нашего bound-сервиса. Теперь сервис будет выглядеть следующим образом:

class BoundService : Service() {    override fun onBind(intent: Intent?): IBinder? {        return object : Calculator.Stub() {            override fun sum(first: Int, second: Int): Int {                return first + second // реализация метода            }        }    }}

Осталось только подключиться к нему на стороне клиента и вызвать метод sum(first, second).

Чтобы мы смогли подключиться к сервису он должен быть доступен сторонним приложениям, поэтому добавим ему в манифест интент-фильтр:

<service    android:name=".BoundService"    android:process=":remote"    android:exported="true">    <intent-filter>        <action android:name="com.example.aidl.REMOTE_CONNECTION" />    </intent-filter></service>

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

Также для подключения требуется отправлять явный (explicit) Intent, т.е он обязан содержать имя пакета и полный путь к классу сервиса, для этого напишем следующий метод:

Создание явного интента
private fun createExplicitIntent(): Intent {    val intent = Intent("com.example.aidl.REMOTE_CONNECTION")    val services = packageManager.queryIntentServices(intent, 0)    if (services.isEmpty()) {        throw IllegalStateException("Приложение-сервер не установлено")    }    return Intent(intent).apply {        val resolveInfo = services[0]        val packageName = resolveInfo.serviceInfo.packageName        val className = resolveInfo.serviceInfo.name        component = ComponentName(packageName, className)    }}

Также, начиная с Android 11 для подключения к сервису в клиентском приложении требуется указать в манифесте тег <queries> и прописать туда <action> сервиса, к которому мы будем подключаться.

Queries tag
<queries>    <intent>        <action android:name="com.example.aidl.REMOTE_CONNECTION" />    </intent></queries>

Ну и последнее что осталось сделать - реализовать интерфейс ServiceConnection, в котором полученный объект типа android.os.IBinder мы приведём к нашему типу com.example.aidl.Calculator:

private var calculator: Calculator? = nullprivate val serviceConnection = object : ServiceConnection {    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {        calculator = Calculator.Stub.asInterface(service)    }    override fun onServiceDisconnected(name: ComponentName?) {        calculator = null    }}override fun onStart() {    super.onStart()    bindService(createExplicitIntent(), serviceConnection, Context.BIND_AUTO_CREATE)}override fun onStop() {    super.onStop()    unbindService(serviceConnection)}

Всё! Теперь у нас есть объект calculator, методы которого выполняются в другом приложении. Вызвав метод calculator.sum(2, 2) на выходе получим 4.

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

Кейс по-сложнее 1 - модель данных

С простым кейсом разобрались, но что если на выходе нужно возвращать не int, а свой тип? Тут мы начинаем сталкиваться с первыми ограничениями.

В AIDL поддерживаются следующие типы данных:
Все примитивы из Java (int, long, char, boolean и т.д)
Массивы примитивов (int[] и т.д.)
Строковые типы CharSequence и String
List<T> содержащий данные типа из этого списка
Map<*, *> содержащий данные типа из этого списка, но без параметризации! Т.е не получится написать Map<String, Integer> - компилятор выдаст ошибку
Parcelable классы, в том числе Bundle
Другие AIDL-интерфейсы (рассмотрим чуть позже)

Поддержка Parcelable меня очень порадовала, ведь можно использовать обычные data классы, помечать их аннотацией @Parcelize и сразу же иметь возможность передавать между процессами, это ли не чудо?!

Давайте усложним наш пример с калькулятором, и будем возвращать не int напрямую, а модель с результатом:

@Parcelizedata class Sum(val sum: Int) : Parcelable
Описание Parcelable прямо в AIDL

В официальной документации сказано, что начиная с Android 10 появилась возможность описания Parcelable классов напрямую в AIDL, однако по какой-то мистической причине у меня это не работает.

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

Чтобы использовать этот класс в AIDL, необходимо создать для него файл-заглушку, причем пакет файла .aidl должен совпадать с пакетом файла .kt. Выглядит это так:

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

Теперь наш калькулятор будет выглядеть следующим образом:

package com.example.aidl;import com.example.aidl.Sum;// Calculator.aidlinterface Calculator {    Sum sum(int first, int second);}

Самое главное - не забывайте про импорты, ведь их придется писать руками, и даже автодополнение вам не поможет.

Кейс по-сложнее 2 - асинхронные вызовы

Если выполнение метода sum(first, second) будет занимать много времени, в конечном счете мы получим зависания на UI треде, а что ещё хуже - ANR. Для избежания такой ситуации можно вынести выполнение метода на другой поток, а результат выполнения возвращать через Callback.

Напишем AsyncCallback.aidl в котором будем получать результат Sum:

package com.example.aidl;import com.example.aidl.Sum;// AsyncCallback.aidlinterface AsyncCallback {    void onSuccess(in Sum sum);}
Про ключевые слова in/out/inout

Ключевые слова in/out/inout позволяют Binder'у пропустить определенный этап маршаллинга данных, повышая производительность IPC транзакций.

Рассмотрим следующий пример - у нас есть метод требующий на вход массив строк помеченный ключевым словом in:

// AidlExample.aidlinterface AidlExample {    void fillArray(in String[] array);}

На стороне клиента мы заполняем этот массив какими-либо данными, например "Cat" и "Dog", а на стороне сервера изменяем значения этих строк на "Apple" и "Strawberry".

Изменится-ли массив на стороне клиента после манипуляций на стороне сервера? Нет, не изменится, потому что in как бы "ограничивает" доступ к оригинальному массиву. Однако на стороне сервера массив будет изменён.

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

Ещё есть ключевое слово out, его отличие от inout в том что массив на стороне клиента будет заполнен теми значениями, которые были установлены на стороне сервера. Пример - на стороне клиента передаем массив из "Cat" и "Dog", а на стороне сервера с массивом ничего не делаем. После выполнения метода оригинальный массив будет содержать null, потому что приложение-сервер его не заполнило.

Учтите что все примитивы в AIDL по-умолчанию помечены словом in, и вы не можете использовать для них свойство out или inout. Но если у вас всё же возникла такая необходимость, рассмотрите вариант использования списка или массива.

Теперь обновим основной класс калькулятора c использованием нового коллбека:

package com.example.aidl;import com.example.aidl.AsyncCallback;// Calculator.aidlinterface Calculator {    void sum(int first, int second, AsyncCallback callback);}

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

Далее останется лишь запустить в сервисе асинхронную операцию и вернуть результат в callback.onSuccess(sum), на этом всё.

Ключевое слово oneway

В предыдущем примере мы запускали задачу в отдельном потоке внутри bound-сервиса, но в AIDL также предусмотрена возможность выполнения самого метода sum(first, second) в другом потоке, для этого его нужно пометить ключевым словом oneway:

package com.example.aidl;import com.example.aidl.AsyncCallback;// Calculator.aidlinterface Calculator {    // P.S слово oneway применимо только к void-методам    oneway void sum(int first, int second, AsyncCallback callback);}

Больше от нас действий не требуется, но учтите, что тоже самое работает и в "обратную" сторону, т.е результат операции также будет получен в другом потоке.

Ещё тут присутствует один неочевидный момент - если клиент и сервер находятся в одном процессе, то такой метод будет выполнен синхронно.

Кейс с закрытием приложения

Официальная документация советует нам всегда отлавливать ошибки типа android.os.RemoteException которые могут возникать при потере соединения.

Если во время асинхронной операции клиентское приложение будет закрыто, то при вызове callback.onSuccess(sum) на стороне сервера мы получим ошибку android.os.DeadObjectException.

Лечится это банально оберткой в try-catch:

// BoundService.ktoverride fun sum(first: Int, second: Int, callback: AsyncCallback?) {    try {        val sum = Sum(first + second)        Thread.sleep(2000) // делаем что-то тяжелое        callback?.onSuccess(sum)    } catch (e: RemoteException) {        Log.e("BoundService", e.message, e)    }}

Тоже самое работает и в обратную сторону - если на момент вызова приложение-сервер недоступно, то на стороне клиента получим android.os.DeadObjectException.

Кейс с обновлением приложения

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

Исправляется очень просто - в момент подключения к сервису регистрируем BroadcastReceiver и слушаем событие ACTION_PACKAGE_REPLACED, а в момент его получения переподключаемся к сервису:

private val appUpdateReceiver = object : BroadcastReceiver() {    override fun onReceive(context: Context?, intent: Intent?) {        if (intent?.data?.schemeSpecificPart == "com.example.appserver") {            unregisterReceiver(this)            reconnect()        }    }}private fun reconnect() {    bindService(createExplicitIntent(), serviceConnection, Context.BIND_AUTO_CREATE)    registerReceiver(appUpdateReceiver, IntentFilter().apply {        addAction(Intent.ACTION_PACKAGE_REPLACED)        addDataScheme("package")    })}

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

Кейс по-сложнее 3 - ошибки

Возьмём предыдущий пример с коллбеком и предположим что у нас появилась необходимость передавать ошибки клиенту, например ArithmeticException при делении на ноль, ну или собственные ошибки унаследованные от RuntimeException.

Добавим в наш коллбек новый метод onError, который будет вызван если на стороне сервера произойдёт ошибка:

package com.example.aidl;import com.example.aidl.Sum;import java.lang.RuntimeException;// AsyncCallback.aidlinterface AsyncCallback {    void onSuccess(in Sum sum);    void onError(in RuntimeException exception);}

Ну вот мы и встретили первую боль - мы не можем передавать ошибки т.к они не реализуют интерфейс Parcelable.

Первое решение которое приходит в голову - создать нечто вроде Parcelable-контейнера для ошибок, т.к всё-таки у нас есть возможность записать Serializable объект в Parcel.

@Parcelizeclass ExceptionHolder(val exception: RuntimeException): Parcelable

Теперь наш коллбек будет выглядеть примерно так:

package com.example.aidl;import com.example.aidl.ExceptionHolder;import com.example.aidl.Sum;// AsyncCallback.aidlinterface AsyncCallback {    void onSuccess(in Sum sum);    void onError(in ExceptionHolder exception);}

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

Первая строчка прямо даёт понять что передавать ошибки между процессами не получится, значит нужно искать другой способ.

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

Таким образом, текущая реализация всего этого будет выглядеть примерно так:

Много кода

Класс, который будет использоваться для передачи ошибки и "воссоздании" её на стороне клиента:

@Parcelizeclass AidlException(    private val errorMessage: String?,    private val errorCode: Int = RUNTIME_EXCEPTION) : Parcelable {    companion object {        const val RUNTIME_EXCEPTION = 1000        const val ARITHMETIC_EXCEPTION = 1001        // TODO другие коды ошибок...    }    // конвертация на стороне клиента    fun toException(): Exception {        return when (errorCode) {            RUNTIME_EXCEPTION -> RuntimeException(errorMessage)            ARITHMETIC_EXCEPTION -> ArithmeticException(errorMessage)            else -> RuntimeException(errorMessage)        }    }}

Коллбек, ответственный за передачу результата между процессами:

package com.example.aidl;import com.example.aidl.AidlException;import com.example.aidl.Sum;// AsyncCallback.aidlinterface AsyncCallback {    void onSuccess(in Sum sum);    void onError(in AidlException aidlException);}

Отправка ошибки на стороне сервиса:

override fun sum(first: Int, second: Int, callback: AsyncCallback?) {    try {        val sum = Sum(first + second)        Thread.sleep(2000) // делаем что-то тяжелое        throw ArithmeticException("Unable to calculate the result") // не получилось :(        callback?.onSuccess(sum)    } catch (e: Throwable) {        Log.e("BoundService", e.message, e)        if (e is ArithmeticException) {            val aidlException = AidlException(e.message, AidlException.ARITHMETIC_EXCEPTION)            callback?.onError(aidlException)        }    }}

Получение и конвертация ошибки на стороне клиента:

calculator?.sum(2, 2, object : AsyncCallback.Stub() {    override fun onSuccess(sum: Sum?) {        Toast.makeText(this@MainActivity, sum.toString(), Toast.LENGTH_SHORT).show()    }    override fun onError(aidlException: AidlException?) {        val exception = aidlException?.toException()        Toast.makeText(this@MainActivity, exception?.message, Toast.LENGTH_SHORT).show()    }})

Кейс по-сложнее 4 - дженерики

В один момент я осознал что слишком ленив, чтобы при создании каждого Parcelable класса создавать для него ещё и файл-заглушку формата .aidl, так что я решил что гораздо удобнее будет создать единый класс для всех транзакций в AIDL, а нужный мне объект хранить внутри такого контейнера.

Если вы думали что весь ужас закончится на передаче ошибок, вы ошибались - в AIDL нет дженериков. Формально они, конечно, есть - мы можем указать тип списка вроде List<String>, но указать тип у собственных Parcelable классов не получится.

Ладно подумал я, пускай их не будет - исходники обоих приложений прямо передо мной, и я точно знаю какой тип данных ожидать на "той" стороне, поэтому мне не составит труда просто передать такой объект из одного приложения в другое. Таким образом можно будет избавится от дублирования .aidl файлов для каждого Parcelable класса.

Давайте напишем такой контейнер и опробуем нашу "технику":

@Parcelizedata class AidlResult<T : Parcelable>(val data: T) : Parcelable

Коллбек с результатом теперь присылает AidlResult вместо Sum:

package com.example.aidl;// опять импорты вручную менять! >:(import com.example.aidl.AidlException;import com.example.aidl.AidlResult;// AsyncCallback.aidlinterface AsyncCallback {    void onSuccess(in AidlResult aidlResult);    void onError(in AidlException aidlException);}

На стороне сервера Sum оборачивается в AidlResult:

override fun sum(first: Int, second: Int, callback: AsyncCallback?) {    try {        val sum = Sum(first + second)        val aidlResult = AidlResult(sum)        Thread.sleep(2000) // делаем что-то тяжелое        callback?.onSuccess(aidlResult)    } catch (e: Throwable) {        // ...    }}

А на стороне клиента приводим data из AidlResult к типу Sum:

calculator?.sum(2, 2, object : AsyncCallback.Stub() {    override fun onSuccess(aidlResult: AidlResult<*>?) {        val sum = aidlResult?.data as? Sum        Toast.makeText(this@MainActivity, sum.toString(), Toast.LENGTH_SHORT).show()    }    override fun onError(aidlException: AidlException?) {        val exception = aidlException?.toException()        Toast.makeText(this@MainActivity, exception?.message, Toast.LENGTH_SHORT).show()    }})

Как думайте, заработает-ли оно с первого раза? Ответ будет отличаться в зависимости от версии kotlin-parcelize плагина.

Когда я только начинал изучать AIDL, в плагине kotlin-android-extensions был баг с типизацией для дженерик-классов который не позволял использовать их в AIDL, т.е аннотация @Parcelize генерировала неподдерживаемый для AIDL код.

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

Единственным решением которое было мной найдено, это использовать вот такой костыль:

Костыль для дженериков
/** * https://youtrack.jetbrains.com/issue/KT-25807 */data class AidlResult<T : Parcelable>(    private var data: T?,    private var classType: Class<T>?) : Parcelable {    @Suppress("UNCHECKED_CAST")    constructor(parcel: Parcel) : this(null, null) {        classType = parcel.readSerializable() as? Class<T>        data = parcel.readParcelable(classType?.classLoader) as? T    }    override fun writeToParcel(parcel: Parcel, flags: Int) {        parcel.writeSerializable(classType)        parcel.writeParcelable(data, flags)    }    override fun describeContents(): Int = 0    companion object CREATOR : Parcelable.Creator<AidlResult<out Parcelable>> {        override fun createFromParcel(parcel: Parcel): AidlResult<out Parcelable> {            return AidlResult(parcel)        }        override fun newArray(size: Int): Array<AidlResult<out Parcelable>?> {            return arrayOfNulls(size)        }    }}

Т.е по сути мы динамически загружаем класс используя ClassLoader, да ещё и лишаемся возможности сгенерировать шаблонный код через @Parcelize.

Я не знаю кого за это благодарить Google или JetBrains, но с переходом на версии Kotlin 1.4.10 -> 1.4.21 и с плагина kotlin-android-extensions на kotlin-parcelize всё заработало и эти костыли не нужны - респект!

Заключение

Работа с IPC в андройде не идеальна - очень расстроила плохая поддержка Android Studio, отсутствие типизации в интерфейсах и механизма для передачи ошибок, но всё-же это был интересный опыт. Надеюсь что статья помогла вам разобраться с работой в AIDL и сэкономит немного времени на разработку приложения.

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

Спасибо!

Подробнее..

Перевод Перевод вводной статьи от разработчиков D-BUS

26.11.2020 08:23:40 | Автор: admin

Руководство по D-BUS

https://dbus.freedesktop.org/doc/dbus-tutorial.html

Red Hat, Inc

<hp@pobox.com>

Дэвид Уиллер

Джон Палмиери

Red Hat, Inc.

<johnp@redhat.com>

Колин Уолтерс

Red Hat, Inc.

<walters@redhat.com>

Версия 0.5.0

Перевод Пластов И.В.

plastov.igor@yandex.ru

Документ в процессе разработки

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

Определенно, приветствуется улучшение руководства - отправляйте свои исправления или предложения в список рассылки. Если вы создаете привязку D-Bus, пожалуйста, добавьте раздел в учебник для вашей привязки, хотя бы небольшой раздел с парой примеров.

Что такое D-Bus?

D-Bus - это система межпроцессного взаимодействия (IPC). Архитектурно он имеет несколько слоев:

  • Библиотекаlibdbus, которая позволяет двум приложениям подключаться друг к другу и обмениваться сообщениями.

  • Исполняемый файл демона шины сообщений, построенный наlibdbus, к которому могут подключаться несколько приложений. Демон может направлять сообщения от одного приложения к нулю или нескольким другим приложениям.

  • Библиотеки враперов или привязок основанных на частичном применении конкретных фреймворков. Например,libdbus-glibиlibdbus-qt. Также существуют привязки к таким языкам, как Python. Эти библиотеки-враперы представляют собой API-интерфейс, который следует использовать большинству людей, поскольку это упрощают детали программирования D-Bus.libdbusпредназначена для низкоуровневого бэкенда в привязках более высокого уровня. Большая часть APIlibdbusполезна только для реализации привязок.

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

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

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

Общесистемный и индивидуальные демоны разделены. Обычный внутрисессионный IPC не использует шину сообщений общесистемного процесса, и наоборот.

Применения D-Bus

В мире существует очень много технологий, заявленная цель которых - межпроцессное взаимодействие или сеть: CORBA, DCE, DCOM, DCOP, XML-RPC, SOAP, MBUS, Internet Communications Engine (ICE) и наверное еще сотни. Каждый из них предназначен для определенных типов использования. D-Bus разработан для двух конкретных случаев:

  • Связь между настольными приложениями в одном рабочем столе; для обеспечения интеграции сеанса рабочего стола в целом и решения проблем жизненного цикла процесса (когда компоненты рабочего стола запускаются и останавливаются).

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

Для случая использования в рамках сеанса рабочего стола рабочие столы GNOME и KDE имеют значительный предыдущий опыт работы с различными решениями IPC, такими как CORBA и DCOP. D-Bus основан на этом опыте и тщательно адаптирован для удовлетворения потребностей, в частности, таких настольных проектов. D-Bus может подходить или не подходить для других приложений; в FAQ есть некоторые сравнения с другими системами IPC.

Проблема, решаемая общесистемным случаем или случаем связи с ОС, хорошо объясняется следующим текстом из проекта Linux Hotplug:

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

Это классическая проблема удаленного системного администратора, когда в случае горячего подключения должно доставляться событие из его домена безопасности (в данном случае ядра операционной системы) в другой (рабочий стол для вошедшего в систему пользователя или удаленного системного администратора). Любой эффективный ответ должен идти другим путем: удаленный домен предпринимает действия, позволяющие ядру выяснить возможности устройства. (Действие часто может быть выполнено асинхронно, например, позволяя новому оборудованию бездействовать до завершения переговоров.) На момент написания этой статьи в Linux не было широко распространенных решений таких проблем. Однако новые разработки D-Bus могут начать решать эту проблему.

D-Bus может оказаться полезным для целей, отличных от тех, для которых он был разработан. Есть общие свойства, которые отличают его от других вариантов IPC:

  • Двоичный протокол, предназначенный для асинхронного использования (в духе протокола X Window System);

  • Постоянные и надежные соединения остаются открытыми все время;

  • Шина сообщений - это демон, а не рой или распределенная архитектура;

  • Многие вопросы реализации и деплоя описаны, а не остаются неоднозначными / настраиваемыми / подключаемыми.

  • Семантика подобна существующей системе DCOP, что позволяет KDE легко ее адаптировать.

Функции безопасности для поддержки режима общесистемной шины сообщений.

Концепции

Некоторые базовые концепции применимы независимо от того, какую платформу приложения вы используете для написания приложения D-Bus. Однако конкретный код, который вы напишете, будет отличаться для приложений GLib, Qt и Python.

Вот диаграмма, которая может помочь вам наглядно представить следующие концепции.

Нативные объекты /и пути к объектам

Вероятно, в вашем фреймворке, определено, что такое объект; обычно это базовый класс. Например:java.lang.Object, GObject, QObject,базовый объект Python или что-то еще. Назовем их нативным объектом.

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

Путь к объекту выглядит как путь в файловой системе, например, объект может называться

/org/kde/kspread/sheet/3/cells/4/5

. Удобочитаемые пути - это хороший тон, но вы можете создать объект с именем

/com/mycompany/c5yo817y0c1y1c5b

, если это имеет смысл для вашего приложения.

Разумно начинать пути к объектам с их пространств имен - с компонентов вашего доменного имени (например,

/org/kde

). Благодаря этому разные модули кода в одном процессе не будут мешать друг другу.

Методы и сигналы

У каждого объекта есть члены. Два вида членов - это методы и сигналы. Методы - это операции, которые могут быть вызваны у объекта, с необязательным вводом (они же аргументы или входные параметры) и выводом (они же возвращаемые значения или исходящие параметры).Сигналы - это широковещательные передачи от объекта всем заинтересованным наблюдателям объекта; сигналы могут содержать полезную нагрузку.

На методы, и сигналы ссылаются по их имени, например Frobate или OnClicked.

Интерфейсы

Каждый объект поддерживает один или несколько интерфейсов. Понимайте интерфейс как именованную группу методов и сигналов, как в GLib, Qt или Java. Интерфейсы определяют тип экземпляра объекта.

DBus идентифицирует интерфейсы с помощью простой строки с именами, например

org.freedesktop.Introspectable

. Большинство привязок будут непосредственно отображать эти имена интерфейсов на соответствующую конструкцию языка программирования, например, на интерфейсы Java или чистые виртуальные классы C++.

Прокси

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

В псевдокоде программирование без прокси может выглядеть так:

Message message = new Message("/remote/object/path", "MethodName", arg1, arg2);          Connection connection = getBusConnection();          connection.send(message);          Message reply = connection.waitForReply(message);          if (reply.isError()) {      } else {        Object returnValue = reply.getReturnValue();      }</pre></div><div class="standard" id="magicparlabel-247" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Программирование с использованием прокси может выглядеть так:</div><div class="float-listings" style="border: 2px solid black; padding: 1ex; margin: 1ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre class="listings">Proxy proxy = new Proxy(getBusConnection(), "/remote/object/path");      Object returnValue = proxy.MethodName(arg1, arg2);</pre></div><h2 class="section_" id="magicparlabel-254" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Шинные имена</h2><div class="standard" id="magicparlabel-260" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Когда приложение подключается к демону шины, демон немедленно присваивает ему имя, называемое уникальным именем подключения.<span>&nbsp;</span><a id="magicparlabel-264"></a>Уникальное имя начинается с символа ':' (двоеточия). Эти имена, во время существования шинного демона никогда не используются повторно - то есть вы знаете, что данное имя всегда будет относиться к одному и тому же приложению. Примером уникального имени может быть<pre class="listings">:34-907</pre>. Цифры после двоеточия не имеют иного смысла, кроме их уникальности.</div><div class="standard" id="magicparlabel-269" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Когда имя сопоставляется с подключением определенного приложения, считается, что это приложение<span>&nbsp;</span><span style="font-style: oblique;">владеет</span><span>&nbsp;</span>этим именем.</div><div class="standard" id="magicparlabel-270" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Приложения могут запрашивать дополнительные общеизвестные (<span style="font-style: oblique;">well-known</span>) имена. Например, вы можете написать спецификацию для определения имени<pre class="listings">com.mycompany.TextEditor</pre>. В вашем определении можно указать, что для владения этим именем приложение должно иметь объект с путём<pre class="listings">/com/mycompany/TextFileManager</pre>,</div><div class="standard" id="magicparlabel-279" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">поддерживающий интерфейс<pre class="listings">org.freedesktop.FileHandler</pre>.</div><div class="standard" id="magicparlabel-284" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Затем приложения, для вызова методов, могут отправлять сообщения на это шинное имя, объект и интерфейс.</div><div class="standard" id="magicparlabel-285" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Вы можете думать об уникальных именах как об IP-адресах, а об общеизвестных именах как о доменных именах. Таким образом,<pre class="listings">com.mycompany.TextEditor</pre>может отображаться например как<pre class="listings">:34-907</pre>так же, как<pre class="listings">mycompany.com</pre>соответствовать чему-то вроде<pre class="listings">192.168.0.5</pre>.</div><div class="standard" id="magicparlabel-302" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Имена, помимо маршрутизации сообщений, имеют второе важное применение. Они используются для отслеживания жизненного цикла. Когда приложение завершает работу (или аварийно закрывается), ядро операционной системы закрывается его соединение с шиной сообщений. Затем шина сообщений отправляет сообщения уведомления, информирующие остальные приложения о том, что имена приложения потеряли своего владельца. Отслеживая эти уведомления, ваше приложение может надежно отслеживать время жизни других приложений.</div><div class="standard" id="magicparlabel-303" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Шинные имена также могут использоваться для координации одноэкземплярных приложений. Если, например, вы хотите быть уверенным, что работает только одно приложение<pre class="listings">com.mycompany.TextEditor</pre>, закрывайте приложение текстового редактора, если такое шинное имя уже имеет владельца.</div><h2 class="section_" id="magicparlabel-308" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Адреса</h2><div class="standard" id="magicparlabel-314" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Приложения, использующие D-Bus, являются либо серверами, либо клиентами. Сервер прослушивает входящие соединения; клиент подключается к серверу. Как только соединение установлено, образуется симметричный поток сообщений. Различие клиент-сервер имеет значение только при настройке соединения.</div><div class="standard" id="magicparlabel-315" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Если вы, используете демон шины, ваше приложение будет клиентом демона шины. То есть демон шины прослушивает соединения, а ваше приложение инициирует соединение с демоном шины.</div><div class="standard" id="magicparlabel-316" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">D-Bus адрес указывает, где сервер будет слушать, и куда будет подключаться клиент. Например, адрес<pre class="listings">unix:path=/tmp/abcdef</pre>указывает, что сервер будет прослушивать сокет домена UNIX с путём<pre class="listings">/tmp/abcdef</pre>, и клиент будет подключаться к этому сокету. Адрес может также определять TCP/IP сокеты или любой другой транспорт, который будет определен в будущих итерациях спецификации D-Bus.</div><div class="standard" id="magicparlabel-325" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">При использовании D-Bus с демоном шины сообщений,<span>&nbsp;</span><span style="font-style: oblique;">libdbus</span><span>&nbsp;</span>автоматически обнаруживает адрес сеансового демона шины, считывая переменную среды. Он обнаруживает демон общесистемной шины, проверяя известный путь сокета домена UNIX (хотя вы можете переопределить этот адрес с помощью переменной среды).</div><div class="standard" id="magicparlabel-326" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Если вы используете D-Bus без демона шины, то вам решать, какое приложение будет сервером, а какое - клиентом, а также указать механизм для согласования адреса сервера. Это нетипичный случай.</div><h2 class="section_" id="magicparlabel-327" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Большаяконцептуальная картина</h2><div class="standard" id="magicparlabel-333" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Собирая все эти концепции воедино, для вызова конкретного метода для конкретного экземпляра объекта, необходимо указать несколько вложенных компонентов:</div><div class="standard" id="magicparlabel-334" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Шинное имя указано в квадратных скобках, чтобы указать, что оно необязательно - вы указываете имя только для маршрутизации вызова метода в нужном приложение при использовании демона шины. Если у вас есть прямое соединение с другим приложением, то демон шины отсутствует и шинные имена не используются.</div><div class="float-listings" style="border: 2px solid black; padding: 1ex; margin: 1ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre class="listings">Адрес -> [Шинное имя] -> Путь -> Интерфейс -> Метод</pre></div><div class="standard" id="magicparlabel-339" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Интерфейс также не является обязательным, в первую очередь по историческим причинам; DCOP не требует указания интерфейса, вместо этого просто запрещает дублирование имен методов в одном экземпляре объекта. Таким образом, D-Bus позволит вам не указывать интерфейс, но если имя вашего метода неоднозначно, то не определено, какой метод будет вызван.</div><h2 class="section_" id="magicparlabel-340" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><a id="sec______________________"></a>За кулисамисообщения</h2><div class="standard" id="magicparlabel-346" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">D-Bus работает, отправляя сообщения между процессами. Если вы используете привязку достаточно высокого уровня, возможно вам не понадобится работать с сообщениями напрямую.</div><div class="standard" id="magicparlabel-347" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Есть 4 типа сообщений:</div><ul class="itemize" id="magicparlabel-348" style="margin-top: 0.7ex; margin-bottom: 0.7ex; margin-left: 3ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><li class="itemize_item">Сообщения о вызове метода запрашивают вызов метода для объекта;</li><li class="itemize_item">Сообщения о завершении метода возвращают результаты вызова метода;</li><li class="itemize_item">Сообщения об ошибках возвращают исключение, возникшее при вызове метода;</li><li class="itemize_item">Сигнальные сообщения - это уведомления о том, что данный сигнал был послан (что произошло событие). Вы также можете понимать это как сообщения о событиях.</li></ul><div class="standard" id="magicparlabel-352" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Вызов метода очень просто сопоставляется с сообщениями: вы отправляете сообщение о вызове метода и получаете в ответ либо сообщение о завершении метода, либо сообщение об ошибке.</div><div class="standard" id="magicparlabel-353" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">У каждого сообщения есть заголовок, содержащий поля, и тело, включающее аргументы. Вы можете думать о заголовке как о маршрутной информации для сообщения, а о теле - как о полезной нагрузке. Поля заголовка могут включать шинное имя отправителя, шинное имя назначения, имя метода или сигнала и так далее. Одно из полей заголовка - это сигнатура типа, описывающая значения, находящиеся в теле. Например, буква i означает 32-битное целое число, поэтому сигнатура ii означает, что полезная нагрузка содержит два 32-битных целых числа.</div><h2 class="section_" id="magicparlabel-354" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><a id="chap_______________"></a>За кулисамивызова метода</h2><div class="standard" id="magicparlabel-360" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Вызов метода в DBus состоит из двух сообщений; сообщение о вызове метода, отправленное из процесса A в процесс B, и ответное сообщение соответствующего метода, отправленное из процесса B в процесс A. И вызов, и ответное сообщение маршрутизируются через демон шины. Вызывающий включает в каждое сообщение о вызове отличающийся серийный номер, ответное сообщение содержит этот же номер, чтобы вызывающий процесс мог сопоставить ответы с вызовами.</div><div class="standard" id="magicparlabel-361" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Сообщение о вызове метода будет содержать любые аргументы метода. Ответное сообщение может указывать на ошибку или содержать данные, возвращаемые методом.</div><div class="standard" id="magicparlabel-362" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Вызов метода в DBus происходит следующим образом:</div><ul class="itemize" id="magicparlabel-363" style="margin-top: 0.7ex; margin-bottom: 0.7ex; margin-left: 3ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><li class="itemize_item">Привязка языка может предоставлять прокси, так что вызов метода внутрипроцессного объекта вызывает метод удаленного объекта в другом процессе. Если это так, приложение вызывает метод на прокси-сервере, и прокси создает сообщение о вызове метода для отправки удаленному процессу.</li><li class="itemize_item">Для более низкоуровневых API приложение может создать сообщение о вызове метода само, без использования прокси.</li><li class="itemize_item">В любом случае сообщение о вызове метода содержит: шинное имя, принадлежащее удаленному процессу, название метода, аргументы метода, путь к объекту внутри удаленного процесса и, опционально, имя интерфейса, определяющего метод.</li><li class="itemize_item">Сообщение о вызове метода отправляется демону шины.</li><li class="itemize_item">Демон шины просматривает шинное имя назначения. Если это имя принадлежит процессу, демон шины перенаправляет вызов метода этому процессу. В противном случае демон шины создает сообщение об ошибке и отправляет его обратно в качестве ответа на сообщение о вызове метода.</li><li class="itemize_item">Принимающий процесс распаковывает сообщение о вызове метода. В простой ситуации низкоуровневого API он может немедленно запустить метод и отправить ответное сообщение метода демону шины. При использовании API привязки высокого уровня, привязка может проверять путь к объекту, интерфейс и имя метода и преобразовывать сообщение вызова метода в вызов метода для нативного объекта (GObject, java.lang.Object, QObject, и т.д.), а затем преобразовать возвращаемое значение из нативного метода в ответное сообщение метода.</li><li class="itemize_item">Демон шины получает ответное сообщение метода и отправляет его процессу, который вызывал метод.</li><li class="itemize_item">Процесс, вызывавший метод, просматривает ответ метода и использует любые возвращаемые значения, находящиеся в ответе. Ответ также может указывать на то, что произошла ошибка. При использовании привязки ответное сообщение метода может быть преобразовано в возвращаемое значение прокси-метода или в исключение.</li></ul><div class="standard" id="magicparlabel-371" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Демон шины никогда не меняет порядок сообщений. То есть, если вы отправите два сообщения о вызове метода одному и тому же получателю, они будут получены в том порядке, в котором они были отправлены. Однако получатель не обязан отвечать на вызовы по порядку; например, он может обрабатывать каждый вызов метода в отдельном потоке и возвращать ответные сообщения в неопределенном порядке в зависимости от того, в каком порядке завершаются потоки. Вызовы методов имеют уникальный серийный номер, используемый вызывающим методом для сопоставления ответных сообщений с сообщениями вызова.</div><h2 class="section_" id="magicparlabel-372" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">За кулисамиизлучения сигнала</h2><div class="standard" id="magicparlabel-378" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Сигнал в DBus состоит из одного сообщения, отправляемого одним процессом любому количеству других процессов. То есть<span>&nbsp;</span><a id="magicparlabel-382"></a>сигнал - это однонаправленная трансляция. Сигнал может содержать аргументы (полезные данные), но поскольку он является широковещательным, он никогда не имеет возвращаемого значения. Сравните это с вызовом метода (см.<span>&nbsp;</span><a href="#chap_______________">#</a>), где сообщение о вызове метода имеет соответствующее ответное сообщение метода.</div><div class="standard" id="magicparlabel-383" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Эмитент (он же отправитель) сигнала не знает получателей сигнала. Получатели регистрируются с помощью демона шины для получения сигналов на основе правил соответствия - эти правила обычно включают отправителя и имя сигнала. Демон шины отправляет каждый сигнал только тем получателям, которые проявили интерес к этому сигналу.</div><div class="standard" id="magicparlabel-384" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Сигнал в DBus передается следующим образом:</div><ul class="itemize" id="magicparlabel-385" style="margin-top: 0.7ex; margin-bottom: 0.7ex; margin-left: 3ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><li class="itemize_item">Создается сигнальное сообщение и отправляется демону шины. При использовании низкоуровневого API это можно сделать вручную, с некоторыми привязками это может быть сделано за вас с помощью привязки, когда нативный объект испускает нативный сигнал или событие.</li><li class="itemize_item">Сигнальное сообщение содержит имя интерфейса, определяющего сигнал, название сигнала, шинное имя процесса, отправляющего сигнал и любые аргументы.</li><li class="itemize_item">Любой процесс на шине сообщений может зарегистрировать правила сопоставления, указывающие, какие сигналы ему интересны. У шины есть список зарегистрированных правил сопоставления.</li><li class="itemize_item">Демон шины исследует сигнал и определяет, какие процессы в нем заинтересованы. Он отправляет этим процессам сигнальное сообщение.</li><li class="itemize_item">Каждый процесс, получивший сигнал, решает, что с ним делать; при использовании привязки, привязка может излучать нативный сигнал для прокси-объекта. При использовании низкоуровневого API процесс может просто взглянуть на отправителя сигнала и имя и решить, что на основании этого сделать.</li></ul><h2 class="section_" id="magicparlabel-390" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Интроспекция</h2><div class="standard" id="magicparlabel-396" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Объекты D-Bus могут поддерживать интерфейс<pre class="listings">org.freedesktop.DBus.Introspectable</pre>. У этого интерфейса есть один метод<span>&nbsp;</span><span style="font-style: oblique;">Introspect</span>, который не принимает аргументов и возвращает строку XML. Строка XML описывает интерфейсы, методы и сигналы объекта. См. Спецификацию D-Bus для получения более подробной информации об этом формате интроспекции.</div><h2 class="section_" id="magicparlabel-406" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">GLib API</h2><div class="standard" id="magicparlabel-407" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Рекомендуемый GLib API для D-Bus - GDBus, который распространяется с GLib начиная с версии 2.26. Здесь он не описана, для получения подробной информации об использовании GDBus см. Документацию GLib по ссылке:</div><div class="standard" id="magicparlabel-408" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">https:<a href="http://personeltest.ru/aways/developer.gnome.org/gio/stable/gdbus-convenience.html">//developer.gnome.org/gio/stable/gdbus-convenience.html</a></div><div class="standard" id="magicparlabel-409" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Также существует более старый API,<span>&nbsp;</span><span style="font-style: oblique;">dbus-glib</span>. Он устарел и не должен использоваться в новом коде. По возможности также рекомендуется переносить существующий код из<span>&nbsp;</span><span style="font-style: oblique;">dbus-glib</span><span>&nbsp;</span>в GDBus.</div><h2 class="section_" id="magicparlabel-410" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Python API</h2><div class="standard" id="magicparlabel-416" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Python API,<span>&nbsp;</span><span style="font-style: oblique;">dbus-python</span>, теперь документирован отдельно в руководстве<span>&nbsp;</span><span style="font-style: oblique;">dbus-python</span></div><div class="standard" id="magicparlabel-417" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><a href="http://personeltest.ru/away/dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html">http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html</a></div><div class="standard" id="magicparlabel-418" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">(также доступен в<span>&nbsp;</span><span style="font-style: oblique;">doc/tutorial.txt</span><span>&nbsp;</span>и<span>&nbsp;</span><span style="font-style: oblique;">doc/tutorial.html</span>, если он собран с помощью<span>&nbsp;</span><span style="font-style: oblique;">python-documenttils</span>, в исходном дистрибутиве<span>&nbsp;</span><span style="font-style: oblique;">dbus-python</span>).</div><h2 class="section_" id="magicparlabel-419" style="font-weight: bold; font-size: x-large; margin-top: 1.3ex; margin-bottom: 0.7ex; text-align: left; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Qt API</h2><div class="standard" id="magicparlabel-425" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Привязка Qt для<span>&nbsp;</span><span style="font-style: oblique;">libdbus</span>, QtDBus, распространяется с Qt начиная с версии 4.2. Здесь она не описана. Для получения подробной информации о том, как использовать QtDBus см. документацию Qt:</div><div class="standard" id="magicparlabel-426" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><a href="http://personeltest.ru/away/qt-project.org/doc/qt-5/qtdbus-index.html">http://qt-project.org/doc/qt-5/qtdbus-index.html</a>.</div><div class="standard" id="magicparlabel-427" style="margin-bottom: 2ex; color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"></div><div class="index chapter" style="color: rgb(0, 0, 0); font-family: &quot;Times New Roman&quot;; font-size: medium; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><h1 class="chapter">I</h1></div><!--EndFragment-->
Подробнее..

Перевод Спецификация D-Bus. Часть 1

02.02.2021 08:07:36 | Автор: admin

Введение

DBus - это простая в использовании система межпроцессного взаимодействия (IPC) с низкими издержками. Более подробно:

  • DBus не требует больших затрат , поскольку использует двоичный протокол и не требует преобразования в текстовый формат, например XML, и обратно. Поскольку D-Bus предназначен в первую очередь для IPC внутри одной машины, и во вторую очередь для IPC в Интернете, то это интересная оптимизация. D-Bus также спроектирован таким образом, чтобы избежать задержек и обеспечить асинхронную работу, как и протокол X.

  • DBus прост в использовании потому, что он работает с сообщениями, а не с потоками байтов, и автоматически решает множество сложных проблем IPC. Кроме того, библиотека DBus спроектирована так, чтобы можно было сделать враппер, позволяющий разработчикам использовать существующие объекты/типы их фреймворков, а не изучать новые, специализированные для IPC.

Базовый протокол DBus - это протокол точка-точка (одноранговый или клиент-сервер), описанный в разделе Протокол сообщений. То есть это система, в которой одно приложение взаимодействует с единственным другим приложением. Однако основным, предполагаемым применением протокола является шина сообщений DBus, описанная в разделе Спецификация шины сообщений. Шина сообщений - это специальное приложение, которое принимает соединения от множества других приложений и пересылает сообщения между ними.

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

DBus разработана для двух конкретных случаев использования:

  • Системная шина для уведомлений от системы к пользовательским сеансам и позволяющая системе запрашивать ввод из пользовательских сеансов.

  • Сессионная шина, используемая для реализации окружения рабочего стола, такого как GNOME и KDE.

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

В то же время, демоны шины предлагают ряд функций, которых нет в других системах IPC, таких как шинные имена с одним владельцем (аналогично селекторам в X Window System), запуск сервисов по запросу и политики безопасности. Во многих отношениях эти функции являются основной причиной разработки D-Bus; если бы единственной целью был IPC - существующих систем было бы достаточно.

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

Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, ОБЯЗАТЕЛЬНО, ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, РЕКОМЕНДУЕТСЯ, МОЖЕТ и ДОПОЛНИТЕЛЬНО в этом документе следует интерпретировать, как описано в RFC 2119. Тем не менее, документ может потребовать серьезной проверки, чтобы убедиться, что это имеет смысл. Кроме того, они не пишутся с заглавной буквы.

Стабильность протокола и спецификации

С 8 ноября 2006г. протокол D-Bus заморожен (разрешены только совместимые расширения). Тем не менее, эта спецификация может потребовать значительных усилий, чтобы сделать возможной функционально совместимую повторную реализацию без оглядки на эталонную реализацию DBus. Таким образом, эта спецификация не имеет маркировки 1.0. Прежде чем маркировать её как 1.0, мы хотели бы, чтобы кто-то приложил значительные усилия для уточнения языка спецификации и расширения спецификации, с целью охватить больше аспектов поведения эталонной реализации.

Пока эта работа не будет завершена, любая попытка повторно реализовать D-Bus, вероятно, потребует изучения эталонной реализации и/или вопросов в списке рассылки D-Bus о предполагаемом поведении. Вопросы в рассылке приветствуются.

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

Система типов

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

Протокол D-Bus не включает в маршалированные данные теги типов; блок маршалированных значений должен иметь сигнатуру известного типа. Сигнатура типа состоит из нуля или нескольких отдельных типов, каждый из которых состоит из одного или нескольких кодов типов.

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

Единый полный тип - это последовательность кодов типа, которая полностью описывает его тип: либо базовый тип, либо один полностью описанный контейнерный тип. Единый полный тип - это код базового типа, код вариантного типа, массив с типом его элементов или структура с полями (все они определены ниже). Таким образом, следующие сигнатуры не являются едиными полными типами:

"aa"

"(ii"

"ii)"

А следующие сигнатуры содержат несколько полных типов:

"ii"

"aiai"

"(ii)(ii)"

Однако, обратите внимание, что единый полный тип может содержать несколько других единых полных типов, содержащих запись struct или dict .

Базовые типы

Простейшие коды типов - это базовые типы, то есть типы, структура которых полностью определяется их односимвольным кодом. Базовые типы состоят из фиксированных типов и строковых типов.

Фиксированные типы - это базовые типы, размер которых имеют фиксированную длину, а именно BYTE, BOOLEAN, DOUBLE, UNIX_FD и целые числа со знаком или без знака длиной 16, 32 или 64 бита.

В качестве простого примера кода типа для 32-разрядного целого числа (INT32) - это символ ASCII i. Таким образом, сигнатура для блока значений, содержащего единственный INT32, будет:

"i"

Блок из значений, содержащий два INT32, будет иметь такую сигнатуру:

"ii"

Характеристики фиксированных типов перечислены в этой таблице.

Обычное имя

ASCII код типа

Расшифровка

BYTE

y(121)

Беззнаковое 8-битное целое число

BOOLEAN

b(98)

Логическое значение: 0 - ложь, 1 - истина, любое другое значение, разрешенное форматом маршалинга, недопустимо

INT16

n(110)

Знаковое (в дополнительном коде) 16-разрядное целое число

UINT16

q(113)

Беззнаковое 16-битное целое число

INT32

i(105)

Знаковое (в дополнительном коде) 32-битное целое

UINT32

u(117)

Беззнаковое 32-битное целое число

INT64

x(120)

Знаковое (в дополнительном коде) 64-битное целое число (мнемоника: x и t - первые символы в шестидесяти (sixty), которые еще не используются для чего-то другого более распространенного)

UINT64

t(116)

Беззнаковое 64-битное целое число

DOUBLE

d(100)

IEEE 754 с плавающей запятой двойной точности

UNIX_FD

h(104)

Беззнаковое 32-разрядное целое число, представляющее индекс во вспомогательном массиве файловых дескрипторов, передаваемых через некоторый, зависящий от платформы, механизм (мнемоника: h отhandle(дескриптор))

Строковые типы это базовые типы переменной длины. Значение любого строкового типа концептуально равно 0 или более кодовым позициям Unicode, закодированным в UTF-8, ни одна из которых не может бытьU + 0000 . Текст UTF-8 должен быть строго проверен: в частности, он не должен содержать слишком длинных последовательностей или кодовых позицийU + 10FFFF

Начиная со спецификации DBus версии 0.21, в соответствии с исправлением Unicode #9, в строках UTF8 разрешены "несимвольные знаки" U + FDD0..U + FDEF, U + nFFFE и U + nFFFF (но обратите внимание, что старые версии DBus эти несимвольные знаки отклоняли).

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

Характеристики строковых типов перечислены в этой таблице.

Условное обозначение

ASCII код типа

Ограничения валидности

STRING

s(115)

Без лишних ограничений

OBJECT_PATH

o(111)

Должен быть синтаксически допустимый объектный путь.

SIGNATURE

g(103)

Ноль или более единых полных типов.

Допустимые объектные пути

Объектный путь - это имя, используемое для ссылки на экземпляр объекта. По идее, каждый участник обмена сообщениями D-Bus может иметь любое количество экземпляров объекта (подумайте об объектах C ++ или Java), и каждый такой экземпляр будет иметь путь. Как и файловая система, экземпляры объектов в приложении образуют иерархическое дерево.

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

разрешено, но не рекомендуется, так как это затрудняет управление версиями интерфейсов. Любые сигналы, испускаемые объектом DBus, имеют связанное с ними уникальное шинное имя, а не его общеизвестное имя. Это означает, что получатели сигналов, чтобы определить какой интерфейс породил сигнал, должны полностью полагаться на имя сигнала и объектный путь.Использование пути к объекту/

Например, если владелец example.com разрабатывает API D-Bus для музыкального проигрывателя, он может использовать для своих объектов иерархию путей к объектам, которые начинаются с /com/example/MusicPlayer1 .

Следующие правила определяют допустимый путь к объекту. Реализации не должны отправлять или принимать сообщения с недопустимыми путями к объектам.

  • Путь может быть любой длины.

  • Путь должен начинаться с символа косой черты ASCII '/' (целое число 47) и должен состоять из элементов, разделенных этим символом.

  • Каждый элемент должен содержать только символы ASCII "[A-Z] [a-z] [0-9] "

  • Никакой элемент не может быть пустой строкой.

  • Символы '/' не могут идти подряд.

  • Завершающий символ '/' не допускается, если путь не является корневым (один символ /).

Допустимые сигнатуры

Реализация не должна отправлять или принимать недопустимые сигнатуры. Допустимые сигнатуры будут соответствовать следующим правилам:

  • Сигнатура - это список единых полных типов. Массивы должны иметь типы элементов, а структуры должны иметь как открывающую, так и закрывающую круглые скобки.

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

  • Максимальная глубина вложенности контейнерных типов составляет 32 кода типа массива и 32 открывающие скобки. Это означает, что максимальная общая глубина рекурсии равна 64 для массива массивов массива структуры структуры структуры , где имеется 32 массива и 32 структуры.

  • Максимальная длина сигнатуры 255.

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

Контейнерные типы

В дополнение к базовым типам существует четыре типа контейнеров: STRUCT, ARRAY, VARIANT и DICT_ENTRY.

имеет кодом типа символ ASCII 'r', но этот код не появляется в сигнатурах. Вместо этого используются символы ASCII '(' и ')' для обозначения начала и конца структуры. Так, например, структура, содержащая два целых числа, будет иметь такую сигнатуру:

"(ii)"

Структуры могут быть вложенными, например, структура, содержащая целое число и другую структуру:

"(i(ii))"

.Блок значений, хранящий эту структуру, будет содержать три целых числа; сигнатура типа позволяет отличить(i(ii)) от(ii)i) ,(iii) илиiii

Код типа STRUCT 'r' в настоящее время в протоколе DBus не используется, но полезен в коде, реализующем протокол. Этот код типа указывается, чтобы позволить такому коду взаимодействовать в непротокольных контекстах.

Пустые структуры не допускаются; между скобками должен быть хотя бы один код типа.

ARRAY в качестве кода типа используется символ ASCII 'a'. Код типа массива должен сопровождаться единым полным типом. Единый полный тип, следующий за массивом, - это тип элементов массива. Итак, простой пример:

"ai"

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

"a(ii)"

Или этот массив массивов целых чисел:

"aai"

VARIANT имеет в качестве кода типа ASCII-символ 'v '. Маршалированное значение типа VARIANT будет иметь как часть значения сигнатуру единого полного типа. За этой сигнатурой будет следовать маршалированное значение этого типа.

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

DICT_ENTRY работает точно так же, как структура, но вместо круглых скобок использует фигурные скобки и имеет больше ограничений. Ограничения: тип встречается только как тип элемента массива; внутри фигурных скобок есть ровно два единых полных типа; первый единый полный тип (ключ) должен быть базовым типом, а не контейнерным. Реализации не должны принимать записи dict вне массивов, не должны принимать записи dict с нулем, одним или более чем двумя полями, а также не должны принимать записи dict с ключами, отличными от базовых типов. Запись dict - это всегда пара ключзначение.

Первое поле в DICT_ENTRY всегда является ключом. Сообщение считается поврежденным, если один и тот же ключ встречается дважды в одном и том же массиве DICTENTRY. Однако по соображениям производительности реализациям не требуется отклонять словари с повторяющимися ключами.

В большинстве языков массив записей dict будет представлен как map , хештаблица или объект dict .

Сводка по типам

В следующей таблице приведены типы D-Bus.

Категория

Условное обозначение

Код

Описание

зарезервированный

INVALID

0 (ASCII NUL)

Недействительный код типа, используемый для завершения сигнатур

фиксированный, базовый

BYTE

121 (ASCII 'y')

8-битное целое число без знака

фиксированный, базовый

BOOLEAN

98 (ASCII 'b')

Логическое значение, 0 - ЛОЖЬ, а 1 - ИСТИНА. Все остальное неверно.

фиксированный, базовый

INT16

110 (ASCII 'n')

16-битное целое число со знаком

фиксированный, базовый

UINT16

113 (ASCII 'q')

16-битное целое число без знака

фиксированный, базовый

INT32

105 (ASCII 'i')

32-битное целое число со знаком

фиксированный, базовый

UINT32

117 (ASCII 'u')

32-битное целое число без знака

фиксированный, базовый

INT64

120 (ASCII 'x')

64-битное целое число со знаком

фиксированный, базовый

UINT64

116 (ASCII 't')

64-битное целое число без знака

фиксированный, базовый

DOUBLE

100 (ASCII 'd')

IEEE 754 двойной

строковый, базовый

STRING

115 (ASCII 's')

Строка UTF-8 (должен быть действительный UTF-8). Должен заканчиваться нулем и не содержать других нулевых байтов.

строковый, базовый

OBJECT_PATH

111 (ASCII 'o')

Имя экземпляра объекта

строковый, базовый

SIGNATURE

103 (ASCII 'g')

Сигнатура типа

контейнер

ARRAY

97 (ASCII 'a')

Массив

контейнер

STRUCT

114 (ASCII 'r'),

40 (ASCII '('),

41 (ASCII ')')

Struct; код типа 114 'r' зарезервирован для использования в привязках и реализациях для представления общей концепции структуры и не должен появляться в сигнатурах, используемых в D-Bus.

контейнер

VARIANT

118 (ASCII 'v')

Тип варианта (тип значения является частью самого значения)

контейнер

DICT_ENTRY

101 (ASCII 'e'),

123 (ASCII '{'),

125 (ASCII '}')

Запись вdictилиmap(массив пар ключ-значение). Код типа 101 e зарезервирован к использованию в привязках и реализациях для представления общей концепцииdictилиdict-entryи не должен появляться в сигнатурах, используемых на D-Bus.

фиксированный, базовый

UNIX_FD

104 (ASCII 'h')

Дескриптор файла Unix

зарезервированный

(зарезервированный)

109 (ASCII 'm')

Зарезервировано для типа"возможно", совместимого с типом в GVariant, и не должно появляться в сигнатурах, используемых на D-Bus, пока не будет указано здесь

зарезервированный

(зарезервированный)

42 (ASCII '*')

Зарезервировано для использования в привязках / реализациях для представления любого одного полного типа и не должно появляться в сигнатурах, используемых на D-Bus.

зарезервированный

(зарезервированный)

63 (ASCII '?')

Зарезервировано для использования в привязках/реализациях для представления любого базового типа и не должно появляться в сигнатурах, используемых на DBus.

зарезервированный

(зарезервированный)

64 (ASCII '@'),

38 (ASCII '&'),

94 (ASCII '^')

Зарезервировано для внутреннего использования привязками/реализациями и не должно появляться в сигнатурах, используемых на DBus.GVariantиспользует эти коды типов для кодирования соглашений о вызовах.

Маршалинг (Сериализация)

D-Bus для своей системы типов определяет формат маршалинга(сериализации), который используется в сообщениях D-Bus. Это не единственный возможный формат маршалинга для системы типов: например, GVariant (часть GLib) использует систему типов DBus, но реализует альтернативный формат маршалинга.

Порядок байтов и выравнивание

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

Блок байтов имеет связанный порядок байтов. Порядок байтов должен быть каким-то образом обнаружен; для сообщений DBus порядок байтов является частью заголовка сообщения, как описано в разделе Формат сообщения. Пока предположим, что известен порядок байтов младшим вперед или старшим вперед (little-endian или bigendian).

Каждое значение в блоке байтов выравнивается естественным образом, например, 4байтовые значения выравниваются по 4байтовой границе, а 8байтовые значения - по 8байтовой границе. Границы вычисляются глобально по отношению к первому байту сообщения. Чтобы правильно выровнять значение, перед значением может потребоваться заполнение. Выравнивающее заполнение всегда должно быть минимально необходимым для правильного выравнивания следующего значения; и оно всегда должно состоять из нулевых байтов. Заполнение выравнивания не должно оставаться неинициализированным (оно не может содержать мусор), и нельзя использовать больше байтов заполнения, чем требуется.

Как исключение из естественного порядка выравнивания значений, STRUCT и DICTENTRY всегда выравниваются по 8байтовой границе, независимо от выравнивания их содержимого.

Маршалинг базовых типов

Чтобы маршалировать и демаршалировать фиксированные типы, вы просто считываете одно значение из блока данных, соответствующего каждому коду типа в сигнатуре. Все целочисленные значения со знаком кодируются в дополнительном коде, значения DOUBLE представляют собой числа с плавающей запятой двойной точности IEEE 754, а значения BOOLEAN кодируются в 32битном формате (из которых используется только младший значащий бит).

Строковые типы (STRING, OBJECTPATH и SIGNATURE) все упорядочиваются как целое число фиксированной длины без знака, содержащее длину переменной части, за которым следуют ненулевые байты текста UTF8, и далее следует один нулевой (nul ) байт, который не является частью текста. Выравнивание строкового типа такое же, как выравнивание n : любое заполнение, необходимое для n , появляется непосредственно перед ним. Никогда не бывает выравнивающего заполнения между n и текстом строки или между текстом строки и конечным nul. Выравнивающее заполнение для следующего значения в сообщении (если оно есть) начинается после завершающего нуля.

Для типов STRING и OBJECTPATH n кодируется 4 байтами (UINT32), что приводит к 4байтовому выравниванию. Для типа SIGNATURE n кодируется как один байт (UINT8). В результате выравнивающее заполнение перед SIGNATURE никогда не требуются.

Например, если текущая позиция кратна 8 байтам от начала сообщения с порядком байтов младшим вперед, строки foo , + и bar будут сериализованы в следующей последовательности:

                                          выравнивание не требуется, поскольку длина кратна 4 0x03 0x00 0x00 0x00                       длина foo = 3                    0x66 0x6f 0x6f        foo                                   0x00   завершающий ноль                                          выравнивание не требуется, поскольку длина кратна 40x01 0x00 0x00 0x00                       length of + = 1                    0x2b                  +                         0x00             завершающий ноль                               0x00 0x00  2 выравнивающих байта чтобы получить ближайшую кратную 4 длину 0x03 0x00 0x00 0x00                       длина bar = 3                    0x62 0x61 0x72        bar                                    0x00  завершающий ноль

Маршалинг контейнеров

Массивы маршалируются как n типа UINT32, содержащее длину данных массива в байтах, за которым следует выравнивающее заполнение до границы выравнивания типа элемента массива, далее следуют байты элементов массива, маршалированных в последовательность. Значение n не включает заполнение после длины или любое другое заполнение после последнего элемента. Т.е. n должно быть кратно количеству элементов в массиве.

Например, если текущая позиция в сообщении кратна 8 байтам и порядок байтов bigendian , массив, содержащий только 64-разрядное целое число 5, будет маршалирован как:

00 00 00 08               n = 8 байт данных00 00 00 00               дополнение до границы 8-байт00 00 00 00  00 00 00 05  первый элемент = 5

Массивы имеют максимальную длину 2 в 26-й степени или 67108864 (64 МиБ). Реализации не должны отправлять или принимать массивы, превышающие эту длину.

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

Варианты маршалируются как SIGNATURE их содержимого (которое должно быть единого полного типа), за которым следует маршалированное значение с типом, заданным этой сигнатурой. Вариант, как и сигнатура имеет 1байтовое выравнивание, это означает, что выравнивающее заполнение перед вариантом никогда не требуется. Использование вариантов не должно приводить к тому, чтобы общая глубина сообщения превышала 64, включая другие типы контейнеров, такие как структуры (См. Вводную статью).

Сводка по маршалированию в D-Bus

Учитывая вышесказанное, типы маршалируются по сети следующим образом:

Условное обозначение

Кодирование

Выравнивание

INVALID

Недопустимый, не может быть маршалирован.

Нет данных

BYTE

Один 8-битный байт.

1

BOOLEAN

Используется UINT32, но допустимыми значениями являются только 0 и 1.

4

INT16

16битовое целое число со знаком в порядке байтов сообщения.

2

UINT16

16битное целое число без знака в байтовом порядке сообщения.

2

INT32

32битное целое число со знаком в порядке байтов сообщения.

4

UINT32

32битовое целое число без знака в байтовом порядке сообщения.

4

INT64

64битное целое число со знаком в порядке байтов сообщения.

8

UINT64

64битное целое число без знака в байтовом порядке сообщения.

8

DOUBLE

64битный IEEE 754 с двойным порядком байтов сообщения.

8

STRING

Сначала UINT32, указывающий длину строки в байтах, за исключением завершающего нулевого значения, затем следуют ненулевые строковые данные заданной длины, за которыми следует завершающий нулевой байт.

4 (по длине)

OBJECT_PATH

Точно так же, как STRING, за исключением того, что содержимое должно быть допустимым путем к объекту (см. Выше).

4 (по длине)

SIGNATURE

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

1

ARRAY

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

4 (по длине)

STRUCT

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

8

VARIANT

Маршалированная SIGNATURE одного полного типа, за которой следует маршалированное значение с типом, указанным в сигнатуре.

1 (выравнивание сигнатуры)

DICT_ENTRY

Идентично STRUCT.

8

UNIX_FD

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

4

Протокол сообщений

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

Тело сообщения состоит из нуля или более аргументов, которые представляют собой типизированные значения, такие как целое число или массив байтов.

Для сериализации данных и заголовка, и тела используется формат и система типов DBus.

Формат сообщения

Сообщение состоит из заголовка и тела.

Заголовок- это блок значений с фиксированной сигнатурой и значением.

Тело- это отдельный блок значений соответствующих сигнатуре, указанной в заголовке.

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

Тело сообщения не обязательно должно заканчиваться на 8байтовой границе.

Максимальная длина сообщения, включая заголовок, заполнение для выравнивания заголовка и тело, составляет 2 в 27-й степени или 134217728 (128 МиБ). Реализации не должны отправлять или принимать сообщения, превышающие этот размер.

Сигнатура заголовка:

"yyyyuua(yv)"

Если расшифровать то, это:

BYTE, BYTE, BYTE, BYTE, UINT32, UINT32, ARRAY of STRUCT of (BYTE,VARIANT)

Эти значения имеют следующий смысл:

Значение

Описание

1й BYTE

Флаг порядка байтов; ASCII 'l' для порядка байтовlittleendianили ASCII 'B' дляbigendian. И заголовок, и тело используют этот порядок.

2й BYTE

Тип сообщения. Неизвестные типы следует игнорировать. Типы, определенные в настоящее время, описаны ниже.

3й BYTE

Побитовая сумма флагов по ИЛИ. Неизвестные флаги следует игнорировать. Определенные в настоящее время флаги описаны ниже.

4й BYTE

Основная версия протокола отправляющего приложения. Если основная версия протокола принимающего приложения не совпадает, приложения не смогут взаимодействовать и соединение D-Bus должно быть разорвано. Основная версия протокола для этой версии спецификации - 1.

1й UINT32

Длина тела сообщения в байтах, начиная с конца заголовка. Заголовок заканчивается после его выравнивающего заполнения до 8байтной границы.

2й UINT32

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

ARRAY of STRUCT

of (BYTE, VARIANT)

Массив из нуля или более полей заголовка, где байт - это код поля, а вариант - значение поля. Тип сообщения определяет, какие поля обязательны.

Типы сообщений, которые могут появиться во втором байте заголовка:

Условное обозначение

Десятичное значение

Описание

INVALID

0

Это недопустимый тип.

METHOD_CALL

1

Вызов метода. Этот тип сообщения может требовать ответа.

METHOD_RETURN

2

Ответ метода с возвращенными данными.

ERROR

3

Ответ об ошибке. Если первый аргумент существует и является строкой, это сообщение об ошибке.

SIGNAL

4

Излучение сигнала.

Флаги, которые могут появляться в третьем байте заголовка:

Условное обозначение

hex

Описание

NO_REPLY_EXPECTED

0x1

Это сообщение не ожидает ответов метода или ошибок, даже если оно имеет тип, который может иметь ответ; ответ следует опустить.

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

NO_AUTO_START

0x2

Шина не должна запускать владельца шинного имени назначения в ответ на это сообщение.

ALLOW_INTERACTIVE_AUTHORIZATION

0x4

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

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

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

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

Если этот флаг не установлен для вызова метода, и сервиса определяет, что запрошенная операция не разрешена без интерактивной авторизации, но может быть разрешена после успешной интерактивной авторизации, она может вернуть ошибкуorg.freedesktop.DBus.Error.InteractiveAuthorizationRequirederror.

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

Поля заголовка

Массив в конце заголовка содержит поля заголовка, где каждое поле представляет собой 1байтовый код поля, за которым следует значение поля. Заголовок должен содержать обязательные поля заголовка для своего типа сообщения и ноль или более любых дополнительных полей заголовка. В будущих версиях этой спецификации протокола могут быть добавлены новые поля. Реализации не должны изобретать свои собственные поля заголовка; новые поля заголовка могут вводить только изменения этой спецификации.

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

Однако реализации не должны отправлять или принимать известные поля заголовка с неправильным типом, хранящимся в значении поля. Так, например, сообщение с полем INTERFACE типа UINT32 будет считаться поврежденным.

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

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

Вот определенные в настоящее время поля заголовка:

Условное обозначение

Десятичный код

Тип

Где требуется

Описание

INVALID

0

N/A

не допускается

Недействительное имя поля (ошибка, если оно появляется в сообщении)

PATH

1

OBJECT_PATH

METHOD_CALL, SIGNAL

Объект, которому отправляется вызов или объект, из которого исходит сигнал. Зарезервирован специальный путь/org/ freedesktop/DBus/Local;реализации не должны отправлять сообщения с этим путем, а эталонная реализация демона шины отключит любое приложение, которое пытается это сделать. Это поле заголовка контролируется отправителем сообщения.

INTERFACE

2

STRING

SIGNAL

Интерфейс, выполняющий вызов метода или из которого исходит сигнал. Необязательный для вызовов методов, требуется для сигналов. Зарезервирован специальный интерфейсorg.freedesktop.DBus.Local; реализации не должны отправлять сообщения с этим интерфейсом, а эталонная реализация демона шины отключит любое приложение, которое пытается это сделать. Это поле заголовка контролируется отправителем сообщения.

MEMBER

3

STRING

METHOD_CALL, SIGNAL

Член, либо имя метода, либо имя сигнала. Это поле заголовка контролируется отправителем сообщения.

ERROR_NAME

4

STRING

ERROR

Название возникшей ошибки, для ошибок

REPLY_SERIAL

5

UINT32

ERROR, METHOD_RETURN

Порядковый номер сообщения, на которое это сообщение является ответом. (Порядковый номер является вторым UINT32 в заголовке.) Это поле заголовка контролируется отправителем сообщения.

DESTINATION

6

STRING

необязательный

Имя соединения, для которого предназначено это сообщение. Это поле обычно имеет смысл только в сочетании с шиной сообщений (см. Раздел Спецификация шины сообщений), но другие серверы могут определять для него свои собственные значения. Это поле заголовка контролируется отправителем сообщения.

SENDER

7

STRING

необязательный

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

SIGNATURE

8

SIGNATURE

необязательный

Сигнатура тела сообщения. Если опущено, предполагается, что это пустая подпись "" (т.е. длина тела должна быть 0). Это поле заголовка контролируется отправителем сообщения.

UNIX_FDS

9

UINT32

необязательный

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

Допустимые имена

Разнообразие имен в сообщениях DBus имеет некоторые ограничения.

Для шинных имен, интерфейсов и членов установлена максимальная длина имени 255.

Имена интерфейсов

Интерфейсы имеют имена с типом STRING, что означает, что они должны соответствовать UTF8. Однако есть также некоторые дополнительные ограничения, которые относятся конкретно к именам интерфейсов:

  • Имена интерфейсов состоят из 2 или более элементов, разделенных точкой ('.'). Все элементы должны содержать хотя бы один символ.

  • Каждый элемент должен содержать только символы ASCII [AZ] [az] [09] _ и не должен начинаться с цифры.

  • Имена интерфейсов не должны превышать максимальную длину имени.

Имена интерфейсов должны начинаться с перевернутого имени домена DNS автора интерфейса (в нижнем регистре), подобно именам интерфейсов в Java. Обычно, остальная часть имени интерфейса состоит из слов, идущих подряд, с начальными заглавными буквами во всех словах (Верблюжий регистр). Можно использовать несколько уровней иерархии. Также неплохо включить в имя основную версию интерфейса и увеличить ее, если внесены несовместимые изменения; таким образом, один объект может при необходимости реализовать несколько версий интерфейса параллельно.

Например, если владелец example.com разрабатывает DBus API для музыкального проигрывателя, он может определить интерфейсы с именами com.example.MusicPlayer1, com.example.MusicPlayer1.Track и com.example.MusicPlayer1.Seekable.

Если имя домена DNS автора содержит символы дефиса/минуса ('-'), которые не разрешены в именах интерфейсов DBus, их следует заменить подчеркиванием. Если имя домена DNS содержит цифру, следующую сразу за точкой ('.'), Что также недопустимо в именах интерфейсов), в имени интерфейса перед этой цифрой должен быть добавлен символ подчеркивания. Например, если владелец 7zip.org определил интерфейс для плагинов вне процесса, он может называться org.7zip.Plugin.

D-Bus не делает различий между концепциями, которые в Java назывались бы классами и интерфейсами: любой из них можно идентифицировать на DBus по имени интерфейса.

Шинные имена

Соединения имеют одно или несколько шинных имен, связанных с ними. У соединения есть ровно одно шинное имя, которое является уникальным именем соединения. Уникальное имя соединения остается у соединения в течение всего его срока жизни. Шинное имя имеет тип STRING, что означает, что оно должно быть допустимым UTF8. Однако есть также некоторые дополнительные ограничения, которые применяются конкретно к шинным именам:

  • Шинные имена, начинающиеся с символа двоеточия (':'), являются уникальными именами соединений. Другие шинные имена называются общеизвестными шинными именами.

  • Шинные имена состоят из 1 или более элементов, разделенных точкой ('.'). Все элементы должны содержать хотя бы один символ.

  • Каждый элемент должен содержать только символы ASCII [AZ] [az] [0-9] -, при этом - не рекомендуется использовать в новых шинных именах. Только элементы, которые являются частью уникального имени соединения, могут начинаться с цифры, элементы в других шинных именах не должны начинаться с цифры.

  • В шинном имени должен быть хотя бы один символ . (точка) (и, следовательно, как минимум два элемента).

  • Шинные имена не должны начинаться с символа . (точка).

  • Длина шинного имени не должна превышать максимальную длину.

Обратите внимание, что символ дефиса ('-') разрешен в шинных именах, но не в именах интерфейсов. Это также проблематично или не разрешено в различных спецификациях и APIинтерфейсах, которые относятся к DBus, таких как идентификаторы приложений Flatpak, интерфейс DBusActivatable в спецификации Desktop Entry, а также соглашение о том, что основной интерфейс приложения и путь к объекту похожи на его шинное имя. Чтобы избежать ситуаций, требующих специальной обработки, рекомендуется, чтобы в новых именах DBus неуклонно заменяли дефис на подчеркивание.

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

Как и в случае с именами интерфейсов, если имя домена DNS автора содержит символы дефиса/минуса, они должны быть заменены знаками подчеркивания, а если оно содержит ведущие цифры, их следует экранировать, добавив знак подчеркивания. Например, если владелец 7-zip.org использовал имя DBus для приложения архивации, оно могло бы называться org.7_zip.Archiver .

Если общеизвестное имя шины подразумевает наличие основного интерфейса, этому главному интерфейсу часто дается то же имя, что и общеизвестное шинное имя, и оно располагается в соответствующем объектном пути. Например, если владелец example.com разрабатывает API D-Bus для музыкального проигрывателя, он может установить, что любое приложение, которое получает хорошо известное имя com.example.MusicPlayer1 , в реализации интерфейса com.example.MusicPlayer1 должно иметь объект, с объектный путём /com/example/MusicPlayer1 .

Имена членов

Имена членов (то есть методов или сигналов):

  • Должны содержать только символы ASCII [A-Z] [a-z] [0-9] _ и не могут начинаться с цифры.

  • Не должны содержать символ "." (точка).

  • Длина не должна превышать максимальную длину имени.

  • Длина должна быть не менее 1 байта.

Обычно имена членов на DBus состоят из прописных слов без знаков препинания (верблюжий регистр). Имена методов обычно должны быть глаголами, такими как GetItems , а имена сигналов обычно должны быть описанием события, например ItemsChanged .

Имена ошибок

Имена ошибок имеют те же ограничения, что и имена интерфейсов и часто содержат .Error.; например, владелец example.com может определить errorcom.example.MusicPlayer1.Error.FileNotFound и com.example.MusicPlayer1.Error.OutOfMemory . Ошибки, определенные в самом DBus, такие как org.freedesktop.DBus.Error.Failed , следуют аналогичному шаблону.

Типы сообщений

Каждый из типов сообщений (METHOD_CALL, METHOD_RETURN, ERROR и SIGNAL) ожидаемо имеет свои собственные соглашения об использовании и поля заголовка. В этом разделе описаны эти соглашения.

Вызов методов

Некоторые сообщения вызывают операцию на удаленном объекте. Они называются сообщениями о вызове методов и имеют тег типа METHO_DCALL. Такие сообщения естественно отображаются в обычной программе в методы объектов.

Сообщение о вызове метода должно иметь поле заголовка MEMBER, указывающее имя метода. Опционально, сообщение может иметь поле INTERFACE, в котором указывается интерфейс, частью которого является метод. Настоятельно рекомендуется включать INTERFACE во все сообщения о вызове методов.

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

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

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

Когда приложение обрабатывает сообщение о вызове метода, оно должно вернуть ответ. Ответ идентифицируется полем заголовка REPLY_SERIAL, указывающим серийный номер METHOD_CALL, на который осуществляется ответ. Ответ может быть одного из двух типов; либо METHOD_RETURN, либо ERROR.

Если ответ имеет тип METHOD_RETURN, аргументами ответного сообщения являются возвращаемое значение (значения) или выходные параметры вызова метода. Если ответ имеет тип ERROR, то возникло исключение и вызов завершился неудачно; возвращаемого значения не будет. Нет смысла отправлять несколько ответов на один и тот же вызов метода.

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

Ответное сообщение METHOD_RETURN или ERROR должно иметь поле заголовка REPLY_SERIAL.

Если сообщение METHOD_CALL имеет флаг NO_REPLY_EXPECTED, то приложение, получающее метод, не должно отправлять ответное сообщение (независимо от того, был ли ответом METHOD_RETURN или ERROR).

Если у сообщения нет флага NO_AUTO_START, если имя пункта назначения не существует, то программа, владеющая именем пункта назначения, будет запущена (активирована) до того, как сообщение будет доставлено. См. Раздел Сервисы запуска шины сообщений (активация) . Сообщение будет удерживаться до тех пор, пока новая программа не будет успешно запущена или потерпит неудачу при старте; в случае неудачи будет возвращена ошибка. Этот флаг уместен только в контексте шины сообщений, он игнорируется при соединении точка-точка без промежуточной шины.

Отображение вызовов методов на API

в сообщение D-Bus.API для DBus могут отображать вызовы метода в вызов метода на определенном языке программирования, таком как C++, или могут отображать вызов метода, описанного на IDL.

В API такого рода аргументы метода часто называются in (входящими) (что подразумевает отправку в METHOD_CALL) или out (исходящие, что подразумевает возвращение с METHOD_RETURN). Некоторые API, такие как CORBA, также имеют аргументы inout, которые отправляются и принимаются, т.е. вызывающий объект передает значение, которое изменяется. В D-Bus аргумент inout эквивалентен аргументу in, за которым следует аргумент out. Вы не можете передавать по сети вещи по ссылке, поэтому inout - это просто иллюзия внутрипроцессного API.

Для метода с нолём или одним возвращаемым значением, за которым следуют ноль или более аргументов, где каждый аргумент может быть in, out или inout, вызывающая сторона создает сообщение, добавляя по порядку каждое in или inout. Аргументы "out" не фигурируют в сообщении вызывающего абонента.

Получатель создает ответ, добавляя сначала возвращаемое значение, если оно есть, а затем по порядку каждый аргумент out или inout . Аргументы "in" не представлены в ответном сообщении.

Обычно, ответы об ошибках в языках отображаются в исключения, если они там есть.

При преобразовании собственных API-интерфейсов в DBus, возможно, было бы неплохо автоматически отображать соглашения об именах DBus (FooBar) на собственные соглашения, такие как fooBar или foobar. Это нормально, если вы можете сказать, что собственный API был специально написан для DBus. Это имеет наибольшее значение при написании реализаций объектов, которые будут экспортироваться по шине. Проксисерверы объектов, используемые для вызова удаленных объектов DBus, вероятно, нуждаются в возможности вызывать любой метод DBus, и, следовательно, подобное волшебное отображение имен может быть проблемой.

Эта спецификация не требует никаких привязок собственного API; приведенное выше является лишь предложенным соглашением для согласованности привязок.

Излучение сигнала

В отличие от вызовов методов, на излучение сигналов нет ответов. Излучение сигнала - это просто одно сообщение типа SIGNAL. Оно должно иметь три поля заголовка: PATH, указывающий на объект, из которого был испущен сигнал, плюс INTERFACE и MEMBER, указывающие полное имя сигнала. Заголовок INTERFACE необходим для сигналов, но не является обязательным для вызовов методов.

Ошибки

Сообщения типа ERROR чаще всего являются ответами на METHOD_CALL, но могут быть возвращены в ответ на любое сообщение. Шина сообщений, например, вернет ERROR в ответ на передачу сигнала, если у шины недостаточно памяти для отправки сигнала.

ERROR может иметь любые аргументы, но если первый аргумент - STRING, это должно быть сообщение об ошибке. Сообщение об ошибке может быть записано в журнал или каким-либо образом показано пользователю.

Нотация в этом документе

В этом документе используется простой псевдо-IDL для описания вызовов и сигналов конкретных методов. Вот пример вызова метода:

org.freedesktop.DBus.StartServiceByName (in STRING name,                                          in UINT32 flags,                                          out UINT32 resultcode)

Это означает, что INTERFACE = org.freedesktop.DBus , MEMBER = StartServiceByName, аргументы METHOD_CALL - STRING и UINT32, аргумент METHODRETURN - UINT32. Помните, что поле MEMBER не может содержать "." (точка), поэтому понятно, что последняя часть имени в IDL является именем члена.

В C ++ это может выглядеть так:

unsigned int org::freedesktop::DBus::StartServiceByName (                                            const char *name,                                            unsigned int flags);

или в равной степени реализуемый вариант где возвращаемое значение может быть сделано аргументом:

void org::freedesktop::DBus::StartServiceByName (const char *name,                                                 unsigned int flags,                                                 unsigned int *resultcode);

Каким будет внешний вид в действительности, зависит от разработчика API. Вы можете разработать API не используя пространство имен C++, применяя STL или Qt, varargs или что-то другое.

Сигналы записываются следующим образом:

org.freedesktop.DBus.NameLost (STRING name)

В сигналах не указывается in или out , потому что возможно только одно направление.

Не особо рекомендуется использовать этот убогий псевдо-IDL в существующих реализациях API; вы можете применять собственную нотацию для используемого вами языка или, например, использовать COM или CORBA IDL.

Недопустимые расширения протокола и спецификации

По соображениям безопасности протокол DBus следует строго анализировать и проверять, за исключением определенных точек расширения. Любые недопустимые нарушения протокола или спецификации должны привести к немедленному разрыву соединения без уведомления на другом конце. Следует внимательно рассмотреть исключения, например, может потребоваться исключение из-за хорошо изученной идиосинкразии общеизвестной реализации. В случаях, когда другой конец соединения является 100% доверенным и заведомо дружественным, пропуск проверки по причинам производительности также может иметь смысл в некоторых случаях.

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

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

  • В протоколе аутентификации (см. Раздел Протокол аутентификации) неизвестные команды приводят к ERROR, а не к разъединению. Это позволяет в будущем расширять протокол. Команды, начинающиеся с EXTENSION, зарезервированы для третьих лиц.

  • Протокол аутентификации поддерживает подключаемые механизмы аутентификации.

  • Формат адреса (см. Раздел Адреса серверов) поддерживает новые виды транспорта.

  • Сообщения с неизвестным типом (что-то кроме METHOD_CALL, METHOD_RETURN, ERROR, SIGNAL) игнорируются. Сообщения неизвестного типа должны быть тем не менее правильно сформированы так же, как и известные сообщения. У них должны быть нормальные заголовок и тело.

  • Поля заголовка с неизвестным или неожиданным кодом поля должны быть проигнорированы, хотя, опять же, они должны быть правильно сформированы.

  • Конечно, могут быть добавлены новые стандартные интерфейсы (с новыми методами и сигналами).

Подробнее..
Категории: Программирование , C , *nix , Linux , Ipc , Dbus

Клиент-серверный IPC на Python multiprocessing

11.01.2021 16:10:01 | Автор: admin

Статья отражает личный опыт разработки CLI приложения для Linux.

В ней рассмотрен способ выполнения привилегированных системных вызовов процессом суперпользователя по запросам управляющей программы через строго описанный API.

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

Введение

Межпроцессное взаимодействие (англ. inter-process communication, IPC) обмен данными между потоками одного или разных процессов. Реализуется посредством механизмов, предоставляемых ядром ОС или процессом, использующим механизмы ОС и реализующим новые возможности IPC. Википедия

У процессов, могут быть разные причины для обмена информацией. На мой взгляд все они являются следствием политики безопасности ядра Unix.

Как известно, ядро Unix это автономная система, которая функционирует без вмешательства человека. Собственно говоря, пользователь это объект операционной системы, который появился чтобы обезопасить ядро от несанкционированного вмешательства.

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

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

Тем не менее таким доступом обладает суперпользователь.

Предпосылки параллелизма

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

В противном случае вам придётся запускать свою программу под рутом.

На этом можно было бы закончить, если бы не проблема доступа к эксклюзивным ресурсам пользовательского окружения из окружения суперпользователя.

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

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

При этом вы можете запросить у процесса в руте исполнение системного вызова из пользовательского процесса при помощи одного из методов IPC.


Таблица методов межпроцессного взаимодействия

Метод

Реализуется ОС или процессом

Неименованный канал

Все ОС, совместимые со стандартом POSIX.

Разделяемая память

Все ОС, совместимые со стандартом POSIX.

Очередь сообщений (Message queue)

Большинство ОС.

Сигнал

Большинство ОС; в некоторых ОС, например, в Windows, сигналы доступны только в библиотеках, реализующих стандартную библиотеку языка Си, и не могут использоваться для IPC.

Почтовый ящик

Некоторые ОС.

Сокет

Большинство ОС.

Именованный канал

Все ОС, совместимые со стандартом POSIX.

Проецируемый в память файл (mmap)

Все ОС, совместимые со стандартом POSIX. При использовании временного файла возможно возникновение гонки. ОС Windows также предоставляет этот механизм, но посредством API, отличающегося от API, описанного в стандарте POSIX.

Обмен сообщениями (без разделения)

Используется в парадигме MPI, Java RMI, CORBA и других.

Файл

Все ОС.

Семафор

Все ОС, совместимые со стандартом POSIX.

Канал

Все ОС, совместимые со стандартом POSIX.


Для своего приложения я выбрал сокеты и написал API для коммуникации между процессами.

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

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

Историческая справка

Традиционно процессы, которые запускаются при загрузке системы и остаются активными в фоне, классифицируются как daemon. Имена исполняемых файлов таких программ по соглашению заканчиваются на d. Пример: systemd.

Программы пользовательского пространства, взаимодействующие с daemon можно назвать управляющими, что также по соглашению отражено в их названиях. Пример: systemctl.

Известны и другие примеры: ssh и sshd.

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

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

Для сервера и клиента я использую одинаковую структуру.

. core  api.py  __init__.py main.py

core это пакет, в который можно положить модули с любой логикой. В модуле api реализованы методы обращения процессов друг к другу.

Реализация API клиента

from multiprocessing.connection import Clientfrom multiprocessing.connection import Listener# адрес сервера (процесса в руте) для исходящих# запросовdaemon = ('localhost', 6000)# адрес клиента (этого процесса) для входящих# ответов от сервераcli = ('localhost', 6001)def send(request: dict) -> bool or dict:    """    Принимает словарь аргументов удалённого метода.    Отправляет запрос, после чего открывет сокет    и ждет на нем ответ от сервера.    """    with Client(daemon) as conn:        conn.send(request)    with Listener(cli) as listener:        with listener.accept() as conn:            try:                return conn.recv()            except EOFError:                return Falsedef hello(name: str) -> send:    """    Формирует уникальный запрос и вызывает функцию    send для его отправки.    """    return send({        "method": "hello",        "name": name    })

В модуле connection пакета multiprocessing есть два класса, реализующих API высокого уровня над низкоуровнивым аналогом стандартной библиотеки socket.

Client класс, который содержит методы отправки дейтаграмм.

Listener принимает дейтаграммы.

Отправляемые запросы содержат название целевого метода сервера.

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

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

В main.py я импортирую модуль api для дальнейшего использования.

from core import apiresponse = api.hello("World!")print(response)

Этот код представлен для демонстрации. В работе я использовал Сlick Framework для создания СLI приложения с опциями, которые вызывают методы API.

Реализация API сервера

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

def hello(request: dict) -> str:    """    Привилегированный системный вызов.    """    return " ".join(["Hello", request["name"])

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

from core import apifrom multiprocessing.connection import Listenerfrom multiprocessing.connection import Client# адрес сервера (этого процесса) для входящих запросовdaemon = ('localhost', 6000)# адрес клиента для исходящих ответовcli = ('localhost', 6001)while True:    with Listener(daemon) as listener:        with listener.accept() as conn:            request = conn.recv()            if request["method"] == "hello":                response = api.hello(request)            with Client(cli) as conn:                conn.send(response)

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

Таким образом он всегда слушает порт 6000 и, при поступлении дейтаграммы, анализирует запрос. Затем он вызывает указанный в запросе метод и возвращает результат исполнения клиенту.

Дополнительно

Советую снабдить свой сервер пакетом systemd, который позволяет программам на Python писать лог в journald.

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

Спасибо за внимание!

Подробнее..

Новый sd-bus API от systemd

09.04.2021 00:16:01 | Автор: admin

В новом выпуске systemd v221 мы представляем API sd-bus, поставляемый со стабильной версией systemd. sd-bus - это наша минимальная библиотека D-Bus IPC на языке программирования Си, поддерживающая в качестве бэкэндов как классическую D-Bus на основе сокетов, так и kdbus. Библиотека была частью systemd в течение некоторого времени, но использовалась только внутри проекта, поскольку мы хотели свободно вносить изменения в API, не затрагивая внешних пользователей. Однако теперь, начиная с v221, мы уверены, что сделали стабильный API.

В этом посте я предоставляю обзор библиотеки sd-bus, краткое повторение основ D-Bus и его концепций, а также несколько простых примеров того, как писать клиенты и сервисы D-Bus с её помощью.

Что такое D-Bus?

Давайте начнем с быстрого напоминания, что на самом деле представляет собой D-Bus. Это мощная универсальная система IPC для Linux и других операционных систем. Он определяет такие понятия, как шины, объекты, интерфейсы, методы, сигналы, свойства. Она предоставляет вам детальный контроль доступа, богатую систему типов, лёгкое обнаружение, самодиагностику, мониторинг, надежную многоадресную рассылку, запуск служб, передачу файловых дескрипторов и многое другое. Есть привязки для многих языков программирования, которые используются в Linux.

D-Bus является основным компонентом систем Linux более 10 лет. Это, безусловно, наиболее широко распространенная локальная система IPC высокого уровня в Linux. С момента создания systemd - это была система IPC, в которой она предоставляла свои интерфейсы. И даже до systemd это была система IPC, которую Upstart использовал для своих интерфейсов. Она используется GNOME, KDE и множеством системных компонентов.

К D-Bus относится как спецификация, так и эталонной реализация. Эталонная реализация предоставляет как компонент сервера шины, так и клиентскую библиотеку. Хотя существует множество других популярных последующих реализаций клиентской библиотеки, как для Си, так и для других языков программирования, единственная широко используемая серверная сторона - это та, которая указана в эталонной реализации. (Однако проект kdbus работает над предоставлением альтернативы этой реализации сервера в качестве компонента ядра.)

D-Bus в основном используется как локальный IPC поверх сокетов AF_UNIX. Однако протокол также можно использовать поверх TCP/IP. Он изначально не поддерживает шифрование, поэтому использование D-Bus напрямую через TCP обычно не является хорошей идеей. Можно объединить D-Bus с транспортом, таким как ssh, чтобы защитить его. systemd использует это, чтобы сделать многие из своих API доступными удаленно.

Часто задаваемый вопрос о D-Bus: почему он вообще существует, учитывая, что сокеты AF_UNIX и FIFO уже есть в UNIX и долгое время успешно используются. Чтобы ответить на этот вопрос, давайте сравним D-Bus с популярными сегодня веб-технологиями: AF_UNIX/FIFO для D-Bus тоже самое, что TCP для HTTP/REST. В то время, как сокеты AF_UNIX/FIFO только перекладывают необработанные байты между процессами, D-Bus определяет фактическую кодировку сообщений и добавляет такие концепции, как транзакция вызова методов, система объектов, механизмы безопасности, многоадресная передача сообщений и многое другое.

Из нашего более чем 10-летнего опыта работы с D-Bus сегодня мы знаем, что, хотя есть некоторые области, в которых мы можем что-то улучшить (и мы работаем над этим, как с kdbus, так и с sd-bus), в целом это очень хорошо спроектированная система, которая выдержала испытание временем, выдержала хорошо и получила широкое признание. Если бы сегодня мы сели и разработали совершенно новую систему IPC, включающую весь опыт и знания, полученные с помощью D-Bus, я уверен, что результат был бы очень близок к тому, что уже есть в D-Bus.

Короче говоря: D-Bus великолепен. Если вы разрабатываете проект для Linux и вам нужен локальный IPC, то он должен быть вашим первым выбором. Не только потому, что D-Bus хорошо спроектирован, но и потому, что есть не так много альтернатив, которые могли бы покрыть аналогичную функциональность.

Для чего подходит sd-bus?

Давайте обсудим, для чего написана библиотека sd-bus, как она соотносится с другими библиотеками D-Bus и почему она может стать библиотекой для вашего проекта.

Для языка программирования Си существуют две популярные библиотеки D-Bus: libdbus, поставляемая в эталонной реализации D-Bus, а также GDBus, компонент GLib, низкоуровневой инструментальной библиотеки GNOME.

Из этих двух библиотек libdbus намного старше, так как она была написана во время составления спецификации. Она была написана с упором на то, чтобы быть переносимой и полезной в качестве серверной части для привязок языков более высокого уровня. Обе эти цели требовали, чтобы API был очень универсальным, в результате чего получился относительно сложный в использовании API в котором отсутствуют элементы, которые делают его легким и интересным для использования из Си. Он предоставляет строительные блоки, но в нём мало инструментов, чтобы упростить строительство дома из них. С другой стороны, библиотека подходит для большинства случаев использования (например, она OOM безопасна, что делает ее подходящей для написания системного программного обеспечения самого низкого уровня) и переносима в операционные системы, такие как Windows или более экзотические UNIX.

GDBus - это гораздо более новая реализация. Она была написана после значительного опыта использования оболочки GLib/GObject вокруг libdbus. GDBus реализована с нуля, не имеет общего кода с libdbus. Её дизайн существенно отличается от libdbus, он содержит генераторы кода, чтобы упростить размещение объектов GObject на шине или взаимодействие с объектами D-Bus как с объектами GObject. Она переводит типы данных D-Bus в GVariant, который является мощным форматом сериализации данных GLib. Если вы привыкли к программированию в стиле GLib, тогда вы почувствуете себя как дома, использовать сервисы и клиенты D-Bus с её помощью намного проще, чем с libdbus.

С sd-bus мы теперь предоставляем третью реализацию, не разделяющую кода ни с libdbus, ни с GDBus. Для нас основное внимание было уделено обеспечению своего рода промежуточного звена между libdbus и GDBus: низкоуровневой библиотекой Си, с которой действительно интересно работать, которая имеет достаточно синтаксического сахара, чтобы упростить создание клиентов и сервисов, но, с другой стороны, более низкоуровневой, чем GDBus/GLib/GObject/GVariant. Чтобы использовать её в различных компонентах системного уровня systemd, она должен быть компактной и безопасной для OOM. Еще одним важным моментом, на котором мы хотели сосредоточиться, была поддержка бэкэнда kdbus с самого начала в дополнение к транспорту сокетов исходной спецификации D-Bus (dbus1). Фактически, мы хотели спроектировать библиотеку ближе к семантике kdbus, чем к dbus1, где-то были бы отличия, но при этом чтобы хорошо охватывались оба транспорта. В отличие от libdbus или GDBus, переносимость не является приоритетом для sd-bus, вместо этого мы стараемся максимально использовать платформу Linux и раскрывать конкретные концепции Linux везде, где это выгодно. Наконец, производительность также была проблемой (хотя и второстепенной): ни libdbus, ни GDBus не побьют рекорды скорости. Мы хотели улучшить производительность (пропускную способность и задержку), но для нас важнее простота и правильность. Мы считаем, что результат нашей работы вполне соответствует нашим целям: библиотеку приятно использовать, она поддерживает kdbus и сокеты в качестве серверной части, относительно минимальна, а производительность существенно выше, чем у libdbus и GDBus.

Вот краткие рекомендации, чтобы решить, какой из трех API использовать для вашего проекта на Си:

  • Если вы разрабатываете проект GLib/GObject, GDBus определенно будет вашим лучшим выбором.

  • Если для вас важна переносимость на ядра, отличные от Linux, включая Windows, Mac OS и другие UNIX, используйте либо GDBus (что более или менее означает использование GLib/GObject), либо libdbus (что требует большого количества ручной работы).

  • В противном случае я бы рекомендовал использовать sd-bus.

(Я не рассматриваю здесь C++, речь идет только о простом Си. Но обратите внимание: если вы используете Qt, то QtDBus является предпочтительным API D-Bus, являясь оболочкой для libdbus.)

Введение в концепции D-Bus

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

  • Шина - это то место, где вы ищете услуги IPC. Обычно существует два типа шин: системная шина, одна на систему, на которой располагаются системные службы; и пользовательская шина, одна на каждого пользователя, на которой располагаются пользовательские службы, такие как адресная книга или почтовый клиент. (Первоначально пользовательская шина была на самом деле сеансовой это значит, что вы получаете несколько шин, если входите в систему много раз как один и тот же пользователь, и в большинстве настроек так и остается, но мы работаем над истинной пользовательской шиной, которая существует в единственном экземпляре для каждого пользователя в системе, независимо от того, сколько раз этот пользователь входит в систему.)

  • Сервис - это программа, которая предлагает некоторый IPC API на шине. Служба идентифицируется именем в обратной нотации доменного имени. Таким образом, служба org.freedesktop.NetworkManager на системной шине - это то место, где доступны API-интерфейсы NetworkManager, а org.freedesktop.login1 на системной шине - это место, где доступны API-интерфейсы systemd-logind.

  • Клиент - это программа, которая использует некоторый IPC API на шине. Он общается с сервисом, контролирует его и, как правило, не предоставляет никаких услуг самостоятельно. При этом границы размыты, и многие службы также являются клиентами других служб. Часто термин узел используется как обобщение для обозначения сервиса или клиента.

  • Путь к объекту - это идентификатор объекта в определенной службе. В некотором смысле это сравнимо с указателем Си, поскольку именно так вы обычно ссылаетесь на объект Си, если пишите объектно-ориентированные программы на Си. Однако указатели Си - это просто адреса памяти, и передача адресов памяти другим процессам не имеет смысла, поскольку они относятся к адресному пространству сервиса и клиент не может получить достап к ним. Таким образом, разработчики D-Bus придумали концепцию пути к объекту, который представляет собой просто строку, которая выглядит как путь в файловой системе. Пример: /org/freedesktop/login1 - это путь к объекту менеджер сервиса org.freedesktop.login1 (который, как мы помним из вышеизложенного, является сервисом systemd-logind). Поскольку пути к объектам структурированы как пути файловой системы, их можно аккуратно упорядочить в виде дерева и получить полное дерево объектов. Например, вы найдете все пользовательские сеансы, которыми управляет systemd-logind в ветке /org/freedesktop/login1/session, к примеру: /org/freedesktop/login1/session/_7, /org/freedesktop/login1/session./_55 и так далее. Как сервисы именуют свои объекты и размещают их в дереве, полностью зависит от их разработчиков.

  • Каждый объект, который определяется путем, имеет один или несколько интерфейсов. Интерфейс - это набор сигналов, методов и свойств (вместе называемых членами), которые связаны друг с другом. Концепция интерфейсов D-Bus на самом деле в значительной степени идентична тому, что вы знаете из языков программирования, таких как Java, которые её поддерживают. Какие интерфейсы реализует объект, определяют разработчики сервиса. Имена интерфейсов имеют обратную нотацию доменных имен, как и имена сервисов. (Да, это, по общему признанию, сбивает с толку, поскольку для простых сервисов довольно часто встречается использование строки имени сервиса также в качестве имени интерфейса.) Тем не менее, несколько интерфейсов стандартизированы, и вы найдете их доступными для многих объектов, реализуемых различными сервисами. В частности, это org.freedesktop.DBus.Introspectable, org.freedesktop.DBus.Peer и org.freedesktop.DBus.Properties.

  • Интерфейс может содержать методы. Слово метод более или менее просто причудливое определение для функции, и этот термин используется почти так же в объектно-ориентированных языках, таких как Java. Наиболее распространенное взаимодействие между узлами D-Bus заключается в том, что один узел вызывает метод на другом узле и получает ответ. Метод D-Bus принимает и возвращает несколько параметров. Параметры передаются безопасным для типов способом, а информация о типе включается в данные интроспекции, которые вы можете запросить у каждого объекта. Обычно имена методов (и других типов членов) следуют синтаксису CamelCase. Например, systemd-logind предоставляет метод ActivateSession в интерфейсе org.freedesktop.login1.Manager, который доступен в объекте /org/freedesktop/login1 сервиса org.freedesktop.login1.

  • Сигнатура описывает набор параметров, которые принимает или возвращает функция (или сигнал, свойство, см. ниже). Это последовательность символов, каждый из которых определяет тип соответствующего параметра. Набор доступных типов довольно мощный. Например, есть более простые типы, такие как s для строки или u для 32-битного целого числа, но также и сложные типы, такие как as для массива строк или a(sb) для массива структур, состоящих из одной строки и одного логического значения. См. Спецификацию D-Bus, где приводится полное описание системы типов. Упомянутый выше метод ActivateSession принимает одну строку в качестве параметра (сигнатура параметра, следовательно, равна s) и ничего не возвращает (сигнатура возврата, следовательно, является пустой строкой). Конечно, сигнатура может быть намного сложнее, другие примеры см. ниже.

  • Сигнал - это еще один тип элемента, определяемый в объектной системе D-Bus. Как и у метода, у него есть сигнатура. Однако они служат разным целям. В то время как в вызове метода один клиент отправляет запрос к одному сервису, и этот сервис возвращает ответ клиенту, сигналы предназначены для общего уведомления узлов. Сервисы отправляют их, когда хотят сообщить одному или нескольким узлам на шине, что что-то произошло или изменилось. В отличие от вызовов методов и их ответов, они обычно транслируются по всей шине. В то время как вызовы/ответы методов используются для дуплексной связи один-к-одному, сигналы обычно используются для симплексной связи один-ко-многим (обратите внимание, что это не является обязательным требованием, их также можно использовать один-к-одному). Пример: systemd-logind передает сигнал SessionNew от своего объекта-менеджера каждый раз, когда пользователь входит в систему, и сигнал SessionRemoved каждый раз, когда пользователь выходит из системы.

  • Свойство - это третий тип элементов, определяемый в объектной системе D-Bus. Это похоже на концепцию свойств, известную в таких языках, как C#. Свойства также имеют сигнатуру. Они представляют собой переменные, предоставляемые объектом, которые могут быть прочитаны или изменены клиентами. Пример: systemd-logind предоставляет свойство Docked с сигнатурой b (логическое значение). Оно отражает, считает ли systemd-logind, что система в настоящее время находится в док-станции (применимо только к ноутбукам).

D-Bus определяет много различных концепций. Конечно, все эти новые концепции могут быть непонятными. Давайте посмотрим на них с другой точки зрения. Я предполагаю, что многие из читателей имеют представление о сегодняшних веб-технологиях, в частности о HTTP и REST. Попробуем сравнить концепцию HTTP-запроса с концепцией вызова метода D-Bus:

  • HTTP-запрос, который вы отправляете в определенной сети. Это может быть Интернет, ваша локальная сеть или корпоративный VPN. В зависимости от того, в какой сети вы отправляете запрос, вы сможете общаться с определённым набором серверов. Это мало чем отличается от шинной концепции D-Bus.

  • Затем в сети вы выбираете конкретный HTTP-сервер для общения. Это примерно сопоставимо с выбором сервиса на конкретной шине.

  • Затем на HTTP-сервере вы запрашиваете конкретный URL-адрес. Часть URL-адреса, определяющая путь (под которой я подразумеваю все после имени хоста сервера, вплоть до последнего /) очень похожа на путь к объекту D-Bus.

  • Файловая часть URL-адреса (под которой я подразумеваю все, что находится после последней косой черты, следующее за путём, который описан выше), определяет фактический вызов, который нужно сделать. В D-Bus это может быть сопоставлено с именем интерфейса и метода.

  • Наконец, параметры HTTP-вызова следуют в пути после знака ?, Они отображаются на сигнатуру вызова D-Bus.

Конечно, сравнение HTTP-запроса с вызовом метода D-Bus похоже на сравнение яблок и апельсинов. Тем не менее, я думаю, что полезно получить представление о том, что чему соответствует.

Из оболочки

Так много о концепциях и стоящей за ними серой теории. Давайте сделаем это увлекательным, давайте посмотрим, как это ощущается в реальной системе.

Некоторое время назад в systemd был включен инструмент busctl, который полезен для изучения и взаимодействия с объектной системой D-Bus. При вызове без параметров он покажет вам список всех узлов, подключенных к системной шине. (Вместо этого используйте --user, чтобы увидеть узлы вашей пользовательской шины):

$ busctlNAME                                       PID PROCESS         USER             CONNECTION    UNIT                      SESSION    DESCRIPTION:1.1                                         1 systemd         root             :1.1          -                         -          -:1.11                                      705 NetworkManager  root             :1.11         NetworkManager.service    -          -:1.14                                      744 gdm             root             :1.14         gdm.service               -          -:1.4                                       708 systemd-logind  root             :1.4          systemd-logind.service    -          -:1.7200                                  17563 busctl          lennart          :1.7200       session-1.scope           1          -[]org.freedesktop.NetworkManager             705 NetworkManager  root             :1.11         NetworkManager.service    -          -org.freedesktop.login1                     708 systemd-logind  root             :1.4          systemd-logind.service    -          -org.freedesktop.systemd1                     1 systemd         root             :1.1          -                         -          -org.gnome.DisplayManager                   744 gdm             root             :1.14         gdm.service               -          -[]

(Я немного сократил вывод, чтобы быть кратким).

Список начинается с узлов, подключенных в данный момент к шине. Они идентифицируются по именам, например ":1.11". В номенклатуре D-Bus они называются уникальными именами. По сути, каждый узел имеет уникальное имя, и они назначаются автоматически, когда узел подключается к шине. Если хотите, они очень похожи на IP-адрес. Вы заметите, что несколько узлов уже подключены, включая сам наш небольшой инструмент busctl, а также ряд системных сервисов. Затем в списке отображаются все текущие сервисы на шине, идентифицируемые по именам сервисов (как обсуждалось выше; чтобы отличить их от уникальных имен, они также называются хорошо известными именами). Во многих отношениях хорошо известные имена похожи на имена хостов DNS, то есть они являются более удобным способом ссылки на узел, но на нижнем уровне они просто сопоставляются с IP-адресом или, в этом сравнении, с уникальным именем. Подобно тому, как вы можете подключиться к хосту в Интернете либо по его имени, либо по его IP-адресу, вы также можете подключиться к узлу шины либо по его уникальному, либо по его общеизвестному имени. (Обратите внимание, что каждый узел может иметь сколько угодно хорошо известных имен, подобно тому, как IP-адрес может иметь несколько имен хостов, ссылающихся на него).

Ладно, это уже круто. Попробуйте сами, на своем локальном компьютере (все, что вам нужно, это современный дистрибутив на основе systemd).

Теперь перейдем к следующему шагу. Посмотрим, какие объекты на самом деле предлагает сервис org.freedesktop.login1:

$ busctl tree org.freedesktop.login1/org/freedesktop/login1  /org/freedesktop/login1/seat   /org/freedesktop/login1/seat/seat0   /org/freedesktop/login1/seat/self  /org/freedesktop/login1/session   /org/freedesktop/login1/session/_31   /org/freedesktop/login1/session/self  /org/freedesktop/login1/user    /org/freedesktop/login1/user/_1000    /org/freedesktop/login1/user/self

Красиво, не правда ли? Что на самом деле еще приятнее и чего не видно в выводе, так это то, что доступно полное автозавершение слов из командной строки: когда вы нажимаете TAB, оболочка автоматически заполняет имена служб за вас. Это замечательный инструмент для исследования объектов D-Bus!

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

$ busctl introspect org.freedesktop.login1 /org/freedesktop/login1/session/_31NAME                                TYPE      SIGNATURE RESULT/VALUE                             FLAGSorg.freedesktop.DBus.Introspectable interface -         -                                        -.Introspect                         method    -         s                                        -org.freedesktop.DBus.Peer           interface -         -                                        -.GetMachineId                       method    -         s                                        -.Ping                               method    -         -                                        -org.freedesktop.DBus.Properties     interface -         -                                        -.Get                                method    ss        v                                        -.GetAll                             method    s         a{sv}                                    -.Set                                method    ssv       -                                        -.PropertiesChanged                  signal    sa{sv}as  -                                        -org.freedesktop.login1.Session      interface -         -                                        -.Activate                           method    -         -                                        -.Kill                               method    si        -                                        -.Lock                               method    -         -                                        -.PauseDeviceComplete                method    uu        -                                        -.ReleaseControl                     method    -         -                                        -.ReleaseDevice                      method    uu        -                                        -.SetIdleHint                        method    b         -                                        -.TakeControl                        method    b         -                                        -.TakeDevice                         method    uu        hb                                       -.Terminate                          method    -         -                                        -.Unlock                             method    -         -                                        -.Active                             property  b         true                                     emits-change.Audit                              property  u         1                                        const.Class                              property  s         "user"                                   const.Desktop                            property  s         ""                                       const.Display                            property  s         ""                                       const.Id                                 property  s         "1"                                      const.IdleHint                           property  b         true                                     emits-change.IdleSinceHint                      property  t         1434494624206001                         emits-change.IdleSinceHintMonotonic             property  t         0                                        emits-change.Leader                             property  u         762                                      const.Name                               property  s         "lennart"                                const.Remote                             property  b         false                                    const.RemoteHost                         property  s         ""                                       const.RemoteUser                         property  s         ""                                       const.Scope                              property  s         "session-1.scope"                        const.Seat                               property  (so)      "seat0" "/org/freedesktop/login1/seat... const.Service                            property  s         "gdm-autologin"                          const.State                              property  s         "active"                                 -.TTY                                property  s         "/dev/tty1"                              const.Timestamp                          property  t         1434494630344367                         const.TimestampMonotonic                 property  t         34814579                                 const.Type                               property  s         "x11"                                    const.User                               property  (uo)      1000 "/org/freedesktop/login1/user/_1... const.VTNr                               property  u         1                                        const.Lock                               signal    -         -                                        -.PauseDevice                        signal    uus       -                                        -.ResumeDevice                       signal    uuh       -                                        -.Unlock                             signal    -         -                                        -

Как и раньше, команда busctl поддерживает автозавершение командной строки, поэтому и имя службы, и путь к объекту легко объединяются в оболочке простым нажатием TAB. Вывод показывает методы, свойства, сигналы одного из объектов сеанса, который в настоящее время доступен через systemd-logind. Есть раздел для каждого интерфейса, который известен объекту. Во втором столбце указывается тип члена. В третьем столбце отображается сигнатура члена. В случае методов, она описывает входные параметры. Четвертый столбец показывает возвращаемые параметры. Для свойств четвертый столбец кодирует их текущее значение.

Пока что мы только исследовали. Теперь сделаем следующий шаг: станем активными - вызовем метод:

# busctl call org.freedesktop.login1 /org/freedesktop/login1/session/_31 org.freedesktop.login1.Session Lock

Я не думаю, что мне нужно больше об этом упоминать, но в любом случае: снова доступно полное автозавершение командной строки. Третий аргумент - это имя интерфейса, четвертый - имя метода, оба могут быть легко заполнены нажатием TAB. В этом случае мы выбрали метод Lock, который активирует блокировку экрана для определенного сеанса. И оп, в тот момент, когда я нажал Enter в этой строке, у меня включилась блокировка экрана (это работает только на оконных менеджерах, которые правильно подключаются к systemd-logind. GNOME работает нормально, и KDE тоже должен работать).

Выбранный нами вызов метода Lock очень прост, поскольку он не принимает никаких параметров и не возвращает их. Конечно, для некоторых вызовов всё может быть сложнее. Вот еще один пример, на этот раз с использованием одного из вызовов на собственной шине systemd для запуска произвольного системного модуля:

# busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager StartUnit ss "cups.service" "replace"o "/org/freedesktop/systemd1/job/42684"

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

Конечно, некоторые параметры вызова методов могут быть намного сложнее, но с busctl их все относительно легко закодировать. См. подробности на странице руководства.

busctl поддерживает ряд других операций. Например, вы можете использовать его для мониторинга трафика D-Bus по мере его возникновения (включая создание файла .cap для использования с Wireshark!) Или вы можете установить или получить определенные свойства. Тем не менее, этот пост должен быть о sd-bus, а не busctl, поэтому давайте кратко остановимся здесь и позвольте мне направить вас на страницу руководства на случай, если вы хотите узнать больше об этом инструменте.

busctl (как и остальная часть системы) реализован с использованием API sd-bus. Таким образом, он раскрывает многие особенности самой sd-bus. Например, вы можете использовать его для подключения к удаленным или контейнерным шинам. Он поддерживает как kdbus, так и классический D-Bus, и многое другое!

sd-bus

Но хватит! Вернемся к теме, поговорим о самой sd-bus.

Набор API sd-bus в основном содержится в заголовочном файле sd-bus.h.

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

  • Поддерживает как kdbus, так и dbus1 в качестве серверной части.

  • Имеет высокоуровневую поддержку подключения к удаленным шинам по ssh и шинам локальных контейнеров ОС.

  • Мощная модель учетных данных для реализации аутентификации клиентов в сервисах. В настоящее время поддерживаются 34 отдельных поля, от PID клиента до контрольной группы или наборов возможностей.

  • Поддержка отслеживания жизненного цикла узлов для автоматического освобождения локальных объектов, когда все узлы, ссылающиеся на них, отключены.

  • Клиент строит эффективное дерево решений, чтобы определить, каким обработчикам доставить входящее сообщение шины.

  • Автоматически переводит ошибки D-Bus в ошибки стиля UNIX и обратно (хотя с потерями), чтобы обеспечить лучшую интеграцию D-Bus в низкоуровневые программы Linux.

  • Мощная, но легкая объектная модель для отображения локальных объектов на шине. При необходимости автоматически генерирует самоанализ.

В настоящее время API полностью не документирован, но мы работаем над завершением набора справочных страниц. Подробности смотрите на всех страницах, начинающихся с sd_bus_.

Вызов метода из Си с помощью sd-bus

Так много о библиотеке в целом. Вот пример подключения к шине и выполнения вызова метода:

#include <stdio.h>#include <stdlib.h>#include <systemd/sd-bus.h>int main(int argc, char *argv[]) {  sd_bus_error error = SD_BUS_ERROR_NULL;  sd_bus_message *m = NULL;  sd_bus *bus = NULL;  const char *path;  int r;  /* Connect to the system bus */  r = sd_bus_open_system(&bus);  if (r < 0) {    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));    goto finish;  }  /* Issue the method call and store the respons message in m */  r = sd_bus_call_method(bus,                         "org.freedesktop.systemd1",           /* service to contact */                         "/org/freedesktop/systemd1",          /* object path */                         "org.freedesktop.systemd1.Manager",   /* interface name */                         "StartUnit",                          /* method name */                         &error,                               /* object to return error in */                         &m,                                   /* return message on success */                         "ss",                                 /* input signature */                         "cups.service",                       /* first argument */                         "replace");                           /* second argument */  if (r < 0) {    fprintf(stderr, "Failed to issue method call: %s\n", error.message);    goto finish;  }  /* Parse the response message */  r = sd_bus_message_read(m, "o", &path);  if (r < 0) {    fprintf(stderr, "Failed to parse response message: %s\n", strerror(-r));    goto finish;  }  printf("Queued service job as %s.\n", path);finish:  sd_bus_error_free(&error);  sd_bus_message_unref(m);  sd_bus_unref(bus);  return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;}

Сохраните этот пример как bus-client.c, а затем соберите его с помощью:

$ gcc bus-client.c -o bus-client `pkg-config --cflags --libs libsystemd`

Будет сгенерирован исполняемый файл bus-client, который вы теперь можете запустить. Обязательно запускайте его как root, поскольку доступ к методу StartUnit является привилегированным:

# ./bus-clientQueued service job as /org/freedesktop/systemd1/job/3586.

И это уже наш первый пример. Он показал, как мы вызывали метод на шине. Фактически вызов метода очень близок к инструменту командной строки busctl, который мы использовали ранее. Я надеюсь, что отрывок из кода не требует дополнительных пояснений. Он должен дать вам представление о том, как писать клиентов D-Bus с помощью sd-bus. Для получения дополнительной информации, пожалуйста, просмотрите заголовочный файл, страницу руководства или даже исходники sd-bus.

Реализация сервиса на Си с помощью sd-bus

Конечно, простой вызов одного метода - довольно упрощенный пример. Давайте посмотрим, как написать сервис шины. Мы напишем небольшой сервис калькулятора. Он предоставляет объект реализующий интерфейс, который предоставляет два метода: один для умножения двух 64-битных целых чисел со знаком, а другой для деления одного 64-битного целого числа со знаком на другое.

#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <systemd/sd-bus.h>static int method_multiply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {  int64_t x, y;  int r;  /* Read the parameters */  r = sd_bus_message_read(m, "xx", &x, &y);  if (r < 0) {    fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));    return r;  }  /* Reply with the response */  return sd_bus_reply_method_return(m, "x", x * y);}static int method_divide(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {  int64_t x, y;  int r;  /* Read the parameters */  r = sd_bus_message_read(m, "xx", &x, &y);  if (r < 0) {    fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));    return r;  }  /* Return an error on division by zero */  if (y == 0) {    sd_bus_error_set_const(ret_error, "net.poettering.DivisionByZero", "Sorry, can't allow division by zero.");    return -EINVAL;  }  return sd_bus_reply_method_return(m, "x", x / y);}/* The vtable of our little object, implements the net.poettering.Calculator interface */static const sd_bus_vtable calculator_vtable[] = {  SD_BUS_VTABLE_START(0),  SD_BUS_METHOD("Multiply", "xx", "x", method_multiply, SD_BUS_VTABLE_UNPRIVILEGED),  SD_BUS_METHOD("Divide",   "xx", "x", method_divide,   SD_BUS_VTABLE_UNPRIVILEGED),  SD_BUS_VTABLE_END};int main(int argc, char *argv[]) {  sd_bus_slot *slot = NULL;  sd_bus *bus = NULL;  int r;  /* Connect to the user bus this time */  r = sd_bus_open_user(&bus);  if (r < 0) {    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));    goto finish;  }  /* Install the object */  r = sd_bus_add_object_vtable(bus,                               &slot,                               "/net/poettering/Calculator",  /* object path */                               "net.poettering.Calculator",   /* interface name */                               calculator_vtable,                               NULL);  if (r < 0) {    fprintf(stderr, "Failed to issue method call: %s\n", strerror(-r));    goto finish;  }  /* Take a well-known service name so that clients can find us */  r = sd_bus_request_name(bus, "net.poettering.Calculator", 0);  if (r < 0) {    fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r));    goto finish;  }  for (;;) {    /* Process requests */    r = sd_bus_process(bus, NULL);    if (r < 0) {      fprintf(stderr, "Failed to process bus: %s\n", strerror(-r));      goto finish;    }    if (r > 0) /* we processed a request, try to process another one, right-away */      continue;    /* Wait for the next request to process */    r = sd_bus_wait(bus, (uint64_t) -1);    if (r < 0) {      fprintf(stderr, "Failed to wait on bus: %s\n", strerror(-r));      goto finish;    }  }finish:  sd_bus_slot_unref(slot);  sd_bus_unref(bus);  return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;}

Сохраните этот пример как bus-service.c, а затем соберите его с помощью:

$ gcc bus-service.c -o bus-service `pkg-config --cflags --libs libsystemd`

А теперь запустим:

$ ./bus-service

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

$ busctl --user tree net.poettering.Calculator/net/poettering/Calculator

Как мы видим, в сервисе есть только один объект, что неудивительно, учитывая, что наш код выше зарегистрировал только один. Давайте посмотрим интерфейсы и члены, которые предоставляет этот объект:

$ busctl --user introspect net.poettering.Calculator /net/poettering/CalculatorNAME                                TYPE      SIGNATURE RESULT/VALUE FLAGSnet.poettering.Calculator           interface -         -            -.Divide                             method    xx        x            -.Multiply                           method    xx        x            -org.freedesktop.DBus.Introspectable interface -         -            -.Introspect                         method    -         s            -org.freedesktop.DBus.Peer           interface -         -            -.GetMachineId                       method    -         s            -.Ping                               method    -         -            -org.freedesktop.DBus.Properties     interface -         -            -.Get                                method    ss        v            -.GetAll                             method    s         a{sv}        -.Set                                method    ssv       -            -.PropertiesChanged                  signal    sa{sv}as  -            -

Как упоминалось выше, библиотека sd-bus автоматически добавила пару универсальных интерфейсов. Но первый интерфейс, который мы видим, на самом деле тот, который мы добавили! Он показывает наши два метода, и оба принимают xx (два 64-битных целых числа со знаком) в качестве входных параметров и возвращают один x. Отлично! Но правильно ли это работает?

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Multiply xx 5 7x 35

Вау! Мы передали два целых числа 5 и 7, и служба фактически умножила их для нас и вернула одно целое число 35! Попробуем другой метод:

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Divide xx 99 17x 5

Ух ты! Он может даже делать целочисленное деление! Фантастика! Но давайте обманем его делением на ноль:

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Divide xx 43 0Sorry, can't allow division by zero.

Отлично! Он прекрасно это обнаружил и вернул чистую ошибку. Если вы посмотрите на приведенный выше пример исходного кода, вы увидите, насколько точно мы сгенерировали ошибку.

И это действительно всё, что у меня есть на сегодня. Конечно, примеры, которые я показал, короткие, и я не буду вдаваться в подробности того, что именно делает каждая строка. Однако этот пост должен быть кратким введением в D-Bus и sd-bus, и это уже слишком много для него...

Надеюсь, этот пост был вам полезен. Если вы заинтересованы в использовании sd-bus для своих собственных программ, я надеюсь, он поможет вам. Если у вас есть дополнительные вопросы, посмотрите (неполные) страницы руководства и спросите нас в IRC или в списке рассылки systemd. Если вам нужно больше примеров, взгляните на дерево исходных текстов systemd, все многочисленные шинные сервисы systemd широко используют sd-bus.

Подробнее..

Interprocess communication с использованием GRPC

15.10.2020 18:13:18 | Автор: admin

Сегодня хочу рассказать о нашем пути реализации межпроцессного взаимодействия между приложениями на NET Core и NET Framework при помощи протокола GRPC. Ирония заключается в том, что GRPC, продвигаемый Microsoft как замена WCF на своих платформах NET Core и NET5, в нашем случае случился именно из-за неполноценной реализации WCF в NET Core.


Я надеюсь эта статья найдется поиском когда кто-то будет рассматривать варианты организации IPC и позволит посмотреть на такое высокоуровневаое решение как GRPC с этой, низкоуровневой, стороны.


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


Контекст


С комбинацией этих двух особенностей мы и столкнулись на текущем проекте: нам понадобилось инициировать работу и получать данные из некоего программно-аппаратного комплекса. Поначалу все выглядело очень неплохо: софтовая часть комплекса поднимает WCF службу, которая принимает команды на выполнение и выплёвывает результаты в файл. Более того, производитель предоставляет SDK с примерами! Что может пойти не так? Всё вполне технологично и современно. Никакого ASTM с палочками-разеделителями, ни даже обмена файлами через общую папку.


Но по какой-то странной причине, WCF служба использует дуплексные каналы и привязку WSDualHttpBinding, которая недоступна под .NET Core 3.1, только в "большом" фреймворке (или уже в "старом"?). При этом дуплексность каналов никак не используется! Она просто есть в описании службы. Облом! Ведь остальной проект живет на NET Core и отказываться от этого нет никакого желания. Придется собирать этот "драйвер" в виде отдельного приложения на NET Framework 4.8 и каким то образом пытаться организовать хождение данных между процессами.


Межпроцессное взаимодействие


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


  • возможность передавать команды, в том числе параметризованные
  • возможность получать ответы о результате выполнения команды
  • возможность асинхронно получать ответы о ходе выполнения долгоиграющей задачи
  • контроль получения сообщений второй стороной
  • Поддержка в Windows (начиная с 7 версии)
  • Поддержка в NET Framework и NET Core
  • Строгая типизация сообщений
  • Расширяемость
  • Поменьше боли

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


Именованные каналы


Да, мы сделали это. В первой итерации были выбраны именно именованные каналы, по которым передавались команды и данные. У нас были абстракции для команды и для данных, и два однонаправленных канала в каждом "соединении". У нас не было контроля получения сообщений, зато была боль с поддержкой стейта канал мог развалиться по каким то странным причинам. У нас не было связного асинхронного потока ответов о прогрессе долгоиграющей операци, зато были отдельные события получения сообщения и серверу надо было передавать исходную команду к которой относится ответ. У нас не было настоящей строгой типизации, а только надежда что на той стороне данные упаковали в правильную "команду" или "ответ". Боль? Боль была, постоянная: трудновоспроизводимые баги, никакущий дебаг, мрак и заброшенность.


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


GRPC


Уныло поглядев на в очередной раз глюканувший софт, рассказывающий о сломанных трубах, было принято решение все переписать. И переписать на GRPC. Почему GRPC? Ну, мы используем его в хвост и гриву для взаимодействия между клиентом и основным сервером приложения. Он произвел впечатление достаточно быстрого и богатого возможностями решения.
Что касается чеклиста с требованиями, то получается примерно так:


  • возможность передавать команды, в том числе параметризованные да, это то что называется Unary call
  • возможность получать ответы о результате выполнения команды то же
  • возможность асинхронно получать ответы о ходе выполнения долгоиграющей задачи да, это server streaming rpc
  • контроль получения сообщений второй стороной обеспечивается транспортным уровнем HTTP/2
  • Поддержка в Windows (начиная с 7 версии) да, но в семёрке есть определённые ограничения
  • Поддержка в NET Framework и NET Core да
  • Строгая типизация сообщений да, обеспечивается форматом protobuf
  • Расширяемость и расширяемость
  • Поменьше боли больно почти не будет , как комарик укусит

Затаскиваем GRPC за 5 минут


Я позволю себе написать очередной tutorial. Туториалов по GRPC много, но они все одинаковые и не об этом, а в начале своего пути по дороге IPC я бы хотел чтобы что-то такое попалось мне на глаза. Все примеры кода есть в репозитории тут.


Подготовка


Наше решение будет состоять из трёх сборок:


  • IpcGrpcSample.CoreClient консольное приложение под NET Core 3.1, будет играть роль клиента RPC
  • IpcGrpcSample.NetServer консольное приложение под NET Framework 4.8, будет за сервер RPC
  • IpcGrpcSample.Protocol библиотека, нацеленная на NET Standard 2.0. В ней будет определен контракт RPC

Для удобства работы отредактируем формат файла проекта на NET Framework под новый лад и снесем Properties\AssemblyInfo.cs


<Project Sdk="Microsoft.NET.Sdk">  <PropertyGroup>    <TargetFramework>net48</TargetFramework>  </PropertyGroup>  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />  <PropertyGroup>...</PropertyGroup>  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">...</PropertyGroup>  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">...</PropertyGroup>  <ItemGroup>    <Reference Include="System" />    <Reference Include="System.Core" />    <Reference Include="System.Xml.Linq" />    <Reference Include="System.Data.DataSetExtensions" />    <Reference Include="Microsoft.CSharp" />    <Reference Include="System.Data" />    <Reference Include="System.Net.Http" />    <Reference Include="System.Xml" />  </ItemGroup>  <ItemGroup>    <None Include="App.config" />  </ItemGroup>  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /></Project>

Настало время тащить NuGet!


  • В проект IpcGrpcSample.Protocol подключаем пакеты Google.Protobuf, Grpc и Grpc.Tools
  • В сервер затащим Grpc, Grpc.Core, Microsoft.Extensions.Hosting и Microsoft.Extensions.Hosting.WindowsServices.
  • В клиента тащим Grpc.Net.Client и OneOf он нам пригодится.

Описываем gRPC службу


Наверно всем уже надоел пример с GreeterService? Давайте что-нибудь повеселее. Сделаем две службы. Одна будет управлять термоциклером-амплификатором, другая будет запускать протокол экстракции на автомтаизированной станции экстракции.


Службы описываются в виде .proto файлов в разделяемой сборке IpcGrpcSample.Protocol. Protobuf-компилятор потом соберет из них нужные нам классы и заготовки под сервисы и клиентов.


Описание службы работы с роботизированной станцией


//указание синтаксиса описанияsyntax = "proto3"; // импорт типа Emptyimport "google/protobuf/empty.proto";// классы будут генерироваться в этом пространстве именoption csharp_namespace = "IpcGrpcSample.Protocol.Extractor"; // описание методов RPC службы управления экстракторомservice ExtractorRpcService {    // унарный вызов "запускающий" операцию  rpc Start (google.protobuf.Empty) returns (StartResponse);  }// ответ на стартmessage StartResponse {    bool Success = 1;}

Описание службы работы с термоциклером


//указание синтаксиса описанияsyntax = "proto3"; // классы будут генерироваться в этом пространстве именoption csharp_namespace = "IpcGrpcSample.Protocol.Thermocycler"; // описание методов RPC службы управления термоциклеромservice ThermocyclerRpcService {    // server-streaming вызов "запускающий эксперимент". На один запрос отправит множество сообщений-ответов, которые будут доступны асинхронно  rpc Start (StartRequest) returns (stream StartResponse);  }// описание сообщения - запроса на запуск экспериментаmessage StartRequest {  // поля запроса - это поле будет названием эксперимента  string ExperimentName = 1;  // а числовое поле - количество циклов, в ходе которых прибор будет "снимать показания" и отправлять назад  int32 CycleCount = 2;}// сообщение из стрима после стартаmessage StartResponse {  // номер цикла  int32 CycleNumber = 1;  // поле в виде конструкции oneof - сообщение может содержать объект одного из типов.   // Что-то вроде discriminated union, но попроще  oneof Content {    // прочитанные данные реакционного блока    PlateRead plate = 2;    // сообщение статуса прибора    StatusMessage status = 3;  }}message PlateRead {  string ExperimentalData = 1;}message StatusMessage {  int32 PlateTemperature = 2;}

Все proto-файлы надо пометить как компилируемые protobuf компилятором. Ручками по одному или добавить в csproj эти строки:


  <ItemGroup>    <Protobuf Include="**\*.proto" />  </ItemGroup>

Строим сервер


На дворе 2020 год и поэтому сервер соберем на базе Hosting абстракций из NET Core. Сразу вставляем такой сниппет в Program.cs:


class Program{    static Task Main(string[] args) => CreateHostBuilder(args).Build().RunAsync();    public static IHostBuilder CreateHostBuilder(string[] args) =>        Host.CreateDefaultBuilder(args)        .UseWindowsService()        .ConfigureServices(services =>        {            services.AddLogging(loggingBuilder =>            {                loggingBuilder.ClearProviders();                loggingBuilder.SetMinimumLevel(LogLevel.Trace);                                    loggingBuilder.AddConsole();            });            services.AddTransient<ExtractorServiceImpl>(); // регистрация зависимостей - сервисов с логикой управления приборами            services.AddTransient<ThermocyclerServiceImpl>();            services.AddHostedService<GrpcServer>(); // регистрация GRPC сервера как HostedService        });}

Обойдемся в примере без конфигурации и логгирования. Реализация служб (контроллеров) будет внедряться в сервер через конструктор.
Код сервера достаточно прямолинеен в методе старта службы сервер поднимается, в методе остановки останавливается. Здесь можно определить будет ли сервер использовать TLS (тогда ему нужен сертификат для шифрования) или трафик будет ходить без шифрования и тогда необходимо использовать ServerCredentials.Insecure. Зачем это может пригодиться и что еще необходимо сделать чтобы гонять голый http/2 трафик в конце.


internal class GrpcServer : IHostedService{    private readonly ILogger<GrpcServer> logger;    private readonly Server server;    private readonly ExtractorServiceImpl extractorService;    private readonly ThermocyclerServiceImpl thermocyclerService;    public GrpcServer(ExtractorServiceImpl extractorService, ThermocyclerServiceImpl thermocyclerService, ILogger<GrpcServer> logger)    {        this.logger = logger;        this.extractorService = extractorService;        this.thermocyclerService = thermocyclerService;        var credentials = BuildSSLCredentials(); // строим креды из сертификата и приватного ключа.         server = new Server //создаем объект сервера        {            Ports = { new ServerPort("localhost", 7001, credentials) }, // биндим сервер к адресу и порту            Services = // прописываем службы которые будут доступны на сервере            {                ExtractorRpcService.BindService(this.extractorService),                ThermocyclerRpcService.BindService(this.thermocyclerService)            }        };                }    /// <summary>    /// Вспомогательный метод генерации серверных кредов из сертификата    /// </summary>    private ServerCredentials BuildSSLCredentials()    {        var cert = File.ReadAllText("cert\\server.crt");        var key = File.ReadAllText("cert\\server.key");        var keyCertPair = new KeyCertificatePair(cert, key);        return new SslServerCredentials(new[] { keyCertPair });    }    public Task StartAsync(CancellationToken cancellationToken)    {        logger.LogInformation("Запуск GRPC сервера");        server.Start();        logger.LogInformation("GRPC сервер запущен");        return Task.CompletedTask;    }    public async Task StopAsync(CancellationToken cancellationToken)    {        logger.LogInformation("Останов GRPC сервера");        await server.ShutdownAsync();        logger.LogInformation("GRPC сервер остановлен");    }}

Осталось закинуть какую нибудь логику в контроллеры и стартовать!
У экстрактора очень простой сервис. Будет через раз отвечать что все получилось или нет:


internal class ExtractorServiceImpl : ExtractorRpcService.ExtractorRpcServiceBase{    private static bool success = true;    public override Task<StartResponse> Start(Empty request, ServerCallContext context)    {        success = !success;        return Task.FromResult(new StartResponse { Success = success });    }}

А у термоциклера организуем что-нибудь повеселее:


internal class ThermocyclerServiceImpl : ThermocyclerRpcService.ThermocyclerRpcServiceBase{    private readonly ILogger<ThermocyclerServiceImpl> logger;    public ThermocyclerServiceImpl(ILogger<ThermocyclerServiceImpl> logger)    {        this.logger = logger;    }    public override async Task Start(StartRequest request, IServerStreamWriter<StartResponse> responseStream, ServerCallContext context)    {        logger.LogInformation("Эксперимент начинается");        var rand = new Random(42);        for(int i = 1; i <= request.CycleCount; ++i)        {            logger.LogInformation($"Отправка цикла {i}");            var plate = new PlateRead { ExperimentalData = $"Эксперимент {request.ExperimentName}, шаг {i} из {request.CycleCount}: {rand.Next(100, 500000)}" };            await responseStream.WriteAsync(new StartResponse { CycleNumber = i, Plate = plate });            var status = new StatusMessage { PlateTemperature = rand.Next(25, 95) };            await responseStream.WriteAsync(new StartResponse { CycleNumber = i, Status = status });            await Task.Delay(500);        }        logger.LogInformation("Эксперимент завершен");    }}

Сервер готов. Можно позапускать консольку и убедиться что GRPC сервер поднимается и останавливается по нажатию на Ctrl-C:


dbug: Microsoft.Extensions.Hosting.Internal.Host[1]      Hosting startinginfo: IpcGrpcSample.NetServer.GrpcServer[0]      Запуск GRPC сервераinfo: IpcGrpcSample.NetServer.GrpcServer[0]      GRPC сервер запущенinfo: Microsoft.Hosting.Lifetime[0]      Application started. Press Ctrl+C to shut down.info: Microsoft.Hosting.Lifetime[0]      Hosting environment: Productioninfo: Microsoft.Hosting.Lifetime[0]      Content root path: C:\Users\user\source\repos\IpcGrpcSample\IpcGrpcSample.NetServer\bin\Debugdbug: Microsoft.Extensions.Hosting.Internal.Host[2]      Hosting startedinfo: Microsoft.Hosting.Lifetime[0]      Application is shutting down...dbug: Microsoft.Extensions.Hosting.Internal.Host[3]      Hosting stoppinginfo: IpcGrpcSample.NetServer.GrpcServer[0]      Останов GRPC сервераinfo: IpcGrpcSample.NetServer.GrpcServer[0]      GRPC сервер остановленdbug: Microsoft.Extensions.Hosting.Internal.Host[4]      Hosting stopped

Соответственно в контроллерах может быть все что угодно: маппинг с транспортной модели на модель бизнеслогики которую нужно дергать из NET Framework, вызов WCF etc. И не надо тащить никакого Kestrel!
Сервер без клиента можно подергать например при помощи grpcurl, но все городилось для межпроцессного взаимодействия. Нужен клиент на NET Core.


Клиентская часть на NET Core


Здесь все проще. Реализуем пару клиентов для сервисов и будем слушать ввод от пользователя.


Код сервиса экстрактора прямолинеен и тривиален. В конструкторе настраивается канал и создается gRPC клиент. Единственный метод дергает RPC метод и разворачивает результат.


class ExtractorClient{    private readonly ExtractorRpcService.ExtractorRpcServiceClient client;    public ExtractorClient()    {        //AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); //без этого кода невозможна коммуникация через http/2 без TLS                var httpClientHandler = new HttpClientHandler        {            ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator // заглушка для самоподписанного сертификата        };        var httpClient = new HttpClient(httpClientHandler);        var channel = GrpcChannel.ForAddress("https://localhost:7001", new GrpcChannelOptions { HttpClient = httpClient });        client = new ExtractorRpcService.ExtractorRpcServiceClient(channel);    }    public async Task<bool> StartAsync()    {        var response = await client.StartAsync(new Empty());        return response.Success;    }}

В клиенте термоциклера используем IAsyncEnumerable<> возвращающим последовательность из монад OneOf<,> эмулируется асинхронная последовательность данных от прибора.


public async IAsyncEnumerable<OneOf<string, int>> StartAsync(string experimentName, int cycleCount){    var request = new StartRequest { ExperimentName = experimentName, CycleCount = cycleCount };    using var call = client.Start(request, new CallOptions().WithDeadline(DateTime.MaxValue)); // настройка времени ожидания    while (await call.ResponseStream.MoveNext())    {        var message = call.ResponseStream.Current;        switch (message.ContentCase)        {            case StartResponse.ContentOneofCase.Plate:                yield return message.Plate.ExperimentalData;                break;            case StartResponse.ContentOneofCase.Status:                yield return message.Status.PlateTemperature;                break;            default:                break;        };    }}

Остальной код клиента тривиален и позволяет только позапускать клиентов в цикле и вывести результаты на консоль.


HTTP/2 и Windows 7


Оказалось, что в седьмой версии Windows нет поддержки TLS в HTTP/2. Поэтому сервер надо бинить немного иначе, используя небезопасные креды:


server = new Server //создаем объект сервера{    Ports = { new ServerPort("localhost", 7001, ServerCredentials.Insecure) }, // биндим сервер к адресу и порту    Services = // прописываем службы которые будут доступны на сервере    {        ExtractorRpcService.BindService(this.extractorService),        ThermocyclerRpcService.BindService(this.thermocyclerService)    }};            

И у клиентов необходимо указать в качестве протокола http, а не https. Но это не все. Необходимо так же сообщить среде, что будет осознанно использоваться небезопасное соединение по http/2:


AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

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

Подробнее..
Категории: Net , Netframework , Grpc , Netcore , Ipc

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru