В мире серверного JavaScript'а я — новичок с чистым, практически незамутнённым разумом. Поэтому когда я узнал о существовании менеджеров процессов, а конкретно — о pm2, то сразу же попробовал применить его для запуска какого-нибудь простейшего backend-сервиса на nodejs в целях самообразования. Мне очень импонирует возможность подключения модулей в JS-коде через import (ES6 modules), т.к. он позволяет использовать один и тот же код как в браузере, так и на серверной стороне, и я запилил простой сервис с ES6-модулями.


Если вкратце, то запустить ES6-версию приложения под pm2 у меня не получилось, для запуска таких приложений лучше использовать либо forever, либо systemd. Под катом — отчёт о результатах для тех, кто любит тексты подлинее.


Введение


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


Тестовый сервис


Для тестирования менеджеров процессов я использовал вот такой код в ES6-сервисе (github repo):


# src/app_es6.mjs
import express from "express";
import mod from "./mod/es6.mjs";

const app = express();
const msg = "Hello World! " + mod.getName();

app.get("/", function (req, res) {
    console.log(msg);
    res.send(msg);
});

app.listen(3000, function () {
    console.log('ES6 app listening on port 3000!');
});

и в ES6-модуле:


# src/mod/es6.mjs
export default {
    getName: function () {
        return "ES6 module is here.";
    }
}

Аналогичный сервис, выполненный c CommonJS-модулями выглядит так:


# src/app_cjs.js
const express = require("express");
const mod = require("./mod/cjs.js");
const app = express();

const msg = "Hello World! " + mod.getName();

app.get("/", function (req, res) {
    console.log(msg);
    res.send(msg);
});

app.listen(3000, function () {
    console.log("CommonJS app listening on port 3000!");
});

CJS-модуль:


# src/mod/cjs.js
module.exports = {
    getName: function () {
        return "CommonJS module is here.";
    }
};

Запуск сервиса без использования менеджера процессов на nodejs v12.14.0:


$  node --experimental-modules ./src/app_es6.mjs    # ES6-service
$  node ./src/app_cjs.js    # CJS-service

pm2


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


CJS-сервис запускается без проблем (pm2 v4.2.1):


$ pm2 start ./src/app_cjs.js -i 4


также без проблем поддерживается заданное количество экземпляров сервиса в кластере:


root@omen17:~# ps -Af | grep app_cjs
alex     29848 29828  0 15:31 ?        00:00:00 node /.../src/app_cjs.js
alex     29855 29828  0 15:31 ?        00:00:00 node /.../src/app_cjs.js
alex     29864 29828  0 15:31 ?        00:00:00 node /.../src/app_cjs.js
alex     29875 29828  0 15:31 ?        00:00:00 node /.../src/app_cjs.js

После "убийства" одного экземпляра (PID 29864) менеджер процессов сразу же поднял новый (PID 30703):


root@omen17:~# kill -s SIGKILL 29864
root@omen17:~# ps -Af | grep app_cjs
alex     29848 29828  0 15:31 ?        00:00:00 node /.../src/app_cjs.js
alex     29855 29828  0 15:31 ?        00:00:00 node /.../src/app_cjs.js
alex     29875 29828  0 15:31 ?        00:00:00 node /.../src/app_cjs.js
alex     30703 29828  7 15:35 ?        00:00:00 node /.../src/app_cjs.js

Но ES6-версия приложения не отрабатывает корректно в pm2. При передаче в nodejs аргумента "--experimental-modules":


$ pm2 start ./src/app_es6.mjs -i 4 --node-args="--experimental-modules"

получается вот такая картина:


В логах видим:


$ pm2 log
...
/home/alex/.pm2/logs/app-es6-error-2.log last 15 lines:
2|app_es6  |     at /usr/lib/node_modules/pm2/node_modules/async/internal/onlyOnce.js:12:16
2|app_es6  |     at WriteStream.<anonymous> (/usr/lib/node_modules/pm2/lib/Utility.js:186:13)
2|app_es6  |     at WriteStream.emit (events.js:210:5)
2|app_es6  |     at internal/fs/streams.js:299:10
2|app_es6  | Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /home/alex/work/sof_es6_pm/src/app_es6.mjs
2|app_es6  |     at Object.Module._extensions..mjs (internal/modules/cjs/loader.js:1029:9)
2|app_es6  |     at Module.load (internal/modules/cjs/loader.js:815:32)
2|app_es6  |     at Function.Module._load (internal/modules/cjs/loader.js:727:14)
2|app_es6  |     at /usr/lib/node_modules/pm2/lib/ProcessContainer.js:297:23
2|app_es6  |     at wrapper (/usr/lib/node_modules/pm2/node_modules/async/internal/once.js:12:16)
2|app_es6  |     at next (/usr/lib/node_modules/pm2/node_modules/async/waterfall.js:96:20)
2|app_es6  |     at /usr/lib/node_modules/pm2/node_modules/async/internal/onlyOnce.js:12:16
2|app_es6  |     at WriteStream.<anonymous> (/usr/lib/node_modules/pm2/lib/Utility.js:186:13)
2|app_es6  |     at WriteStream.emit (events.js:210:5)
2|app_es6  |     at internal/fs/streams.js:299:10

То есть, по факту pm2 не может без транспиляции запускать скрипты, в которых используются ES6-модули. Последний issue на эту тему создан 5 декабря 2019 (примерно месяц назад).


forever


forever является следующим по популярности менеджером процессов после pm2 (npmtrends). Это более старый проект (начат в 2010 году против 2013 для pm2), но у него более узкий фокус по функциональности, чем у pm2. forever "заточен" на постоянное поддержание работоспособности процесса без всяких дополнительных pm2-плюшек в виде балансировки нагрузки и мониторинга используемых ресурсов. Судя по частоте коммитов проект находится в стабильном состоянии (фазу активного развития уже прошёл) и каких-то новых функций от него ждать не приходится. Я не нашёл способа передачи аргументов в nodejs из командной строки при запуске forever, но такая возможность есть, если использовать конфигурационный файл:


{
  "uid": "app_es6",
  "max": 5,
  "spinSleepTime": 1000,
  "minUptime": 1000,
  "append": true,
  "watch": false,
  "script": "src/app_es6.mjs",
  "command": "node --experimental-modules"
}

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


$ forever start forever.es6.json
...
$ forever list
info:    Forever processes running
data:        uid     command                     script          forever pid  id logfile                           uptime      
data:    [0] app_es6 node --experimental-modules src/app_es6.mjs 3972    3979    /home/ubuntu/.forever/app_es6.log 0:0:0:3.354 

Вот сами процессы:


$ ps -Af | grep es6
ubuntu    3972     1  0 12:01 ?        00:00:00 /usr/bin/node /usr/lib/node_modules/forever/bin/monitor src/app_es6.mjs
ubuntu    3979  3972  0 12:01 ?        00:00:00 node --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs

При "убийстве" процесса (PID 3979) менеджер исправно поднимает новый (PID 4013):


$ kill -s SIGKILL 3979
ubuntu@vsf:~/sof_es6_pm$ ps -Af | grep es6
ubuntu    3972     1  0 12:01 ?        00:00:00 /usr/bin/node /usr/lib/node_modules/forever/bin/monitor src/app_es6.mjs
ubuntu    4013  3972  4 12:10 ?        00:00:00 node --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs

forever прекрасно справляется с запуском приложения, использующего ES6-модули, но возникает вопрос, зачем тянуть на linux-системы forever, если подобной функциональности можно добиться и через средства самой ОС?


systemd


systemd позволяет создавать сервисы в linux-среде и контролировать их запуск, в том числе и в случае их внезапного падения. Достаточно создать unit-файл с описанием сервиса (./app_es6.service):


[Unit]
Description=Simple web server with ES6 modules.
After=network.target

[Service]
Type=simple
Restart=always
PIDFile=/run/app_es6.pid
WorkingDirectory=/home/ubuntu/sof_es6_pm
ExecStart=/usr/bin/nodejs --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs

[Install]
WantedBy=multi-user.target

и залинковать его в каталог /etc/systemd/system (в unit-файле пути должны быть абсолютными). За рестарт сервиса в случае его внезапного останова отвечает опция:


Restart=always

Запуск сервиса осуществляется так:


# systemctl start app_es6.service
# systemctl status app_es6.service 
? app_es6.service - Simple web server with ES6 modules.
   Loaded: loaded (/home/ubuntu/sof_es6_pm/app_es6.service; linked; vendor preset: enabled)
   Active: active (running) since Thu 2020-01-02 11:09:42 UTC; 9s ago
 Main PID: 2184 (nodejs)
    Tasks: 11 (limit: 4662)
   CGroup: /system.slice/app_es6.service
           L-2184 /usr/bin/nodejs --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs

Jan 02 11:09:42 vsf systemd[1]: Started Simple web server with ES6 modules..
Jan 02 11:09:42 vsf nodejs[2184]: (node:2184) ExperimentalWarning: The ESM module loader is experimental.
Jan 02 11:09:42 vsf nodejs[2184]: ES6 app listening on port 3000!

При "убийстве" процесса (PID 2184) systemd исправно поднимает новый (PID 2233):


# ps -Af | grep app_es6
root      2184     1  0 11:09 ?        00:00:00 /usr/bin/nodejs --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs
# kill -s SIGKILL 2184
# ps -Af | grep app_es6
root      2233     1  3 11:10 ?        00:00:00 /usr/bin/nodejs --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs

Т.е., systemd делает то же самое, что и forever, но на более фундаментальном уровне.


StrongLoop


При обзоре вариантов имплементаций менеджеров процессов часто всплывает StrongLoop. Однако очень сильно похоже, что этот проект перестал развиваться (последняя версия 6.0.3 вышла 3 года назад). Мне не удалось его даже установить на Ubuntu 18.04 через npm:


# npm install -g strongloop
npm WARN deprecated swagger-ui@2.2.10: No longer maintained, please upgrade to swagger-ui@3.
...
npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2020-01-02T11_25_15_473Z-debug.log

Через yarn пакет установился, несмотря на большое количество сообщений о deprecated версиях зависимостей и ошибок установки, тем не менее, от изучения StronLoop'а я отказался.


Инструменты разработчика


Очень часто рядом с pm2 и forever встречаются такие пакеты, как nodemon, watch, onchange. Эти инструменты не являются менеджерами процессов, но позволяют мониторить изменения в файлах и выполнять команды, привязанные к этим изменениям (в том числе, и перезапускать приложение).


Резюме


Менеджер процессов, подобный pm2, является очень полезным сервисом в мире серверного JS. Но, к сожалению, сам pm2 не позволяет запускать современные nodejs-приложения (в частности — с ES6-модулями). Так как я не очень люблю транспиляцию, то наиболее приемлемым на данный момент менеджером процессов в nodejs для меня является традиционный systemd (или его альтернативы). Однако я с радостью буду использовать pm2, как только pm2 сможет поддерживать приложения с ES6-модулями.

Комментарии (34)


  1. vasyapivo
    02.01.2020 19:43
    -1

    Современным способом запуска node-пиложений является
    CMD [ «node», «index.js» ]


    1. Akuma
      02.01.2020 20:25

      Ну кстати да, контейнеры так-то удобнее будут. Упало приложение — упал контейнер.


      1. apapacy
        02.01.2020 21:14

        С контейнерами все отлично если это под kubernetes — в остальных случаях скорее больше чем меньше проблем.


      1. flancer Автор
        02.01.2020 22:29
        +1

        А почему контейнер должен упасть, если упало приложение? Я с контейнерами знаком слабо, но по моим представлениям так быть не должно (по крайней мере в docker'е).


        1. VolCh
          03.01.2020 09:20

          Докер (или надстройки над ним) заботятся о замене контейнера, если основной процесс в нём упал. Процесс в контейнере не демонизируется даже.


        1. Yeah
          03.01.2020 23:03

          Именно так и должно быть. контейнер обязан падать при падении приложения.


    1. flancer Автор
      02.01.2020 22:31

      Заинтригован. Не могли бы вы дать ссылку на описание этого способа запуска?


      1. apapacy
        03.01.2020 02:41
        +2

        CMD это имеется в виду ситнаксис Dockerfile например можно почитать в не моей статье https://habr.com/ru/company/southbridge/blog/329138/


        Если основной процесс валится то и контейнер валится. Можно этого избежать если основной просеыы будет например supervisor но это будет уже не docker way


        Кстати supervisor это еще один способ запустить то что нужно.


        Проблема докер-контейнеров в том что для реальной работы на проде они требуют оркестратор в качестве которого выступает например nomad или cubernetes — которые как раз и следят чтобы процесс был рабочий и в случае ошибки запускают новый процесс. Но по сравнению с pm2 для оркестратура нужно еще очень и очень много чего.


        1. VolCh
          03.01.2020 09:25

          restart always или on failure базовая фича докера. Для перезапуска дополнительно оркестраторы не нужны. Они для реплицирования и обнаружения (связи) сервисов прежде всего


          1. apapacy
            03.01.2020 10:12
            +1

            Речь скорее о том что запускать контейнеры на проде командной строкой в 100500 символов не так уж удобно. А от этой командной строки как раз зависит что и как собственно будет работать


            Например если я хочу разобраться как работает хостовый не в докере nginx или mysql я изучаю его конфиги. В случае докере мне для этого нужно ещё знать командную строку, dockerfile которого может и не быть в исходниках а где-то взят готовый чужой имидж.


            1. t_kanstantsin
              03.01.2020 16:10

              Какие-то надуманные проблемы. Чтобы запускать сервер — надо будет знать 100500 символов настройки этого сервера (установка всего и вся, настройка портов, копирование конфигов). А переехать на другой сервер — это значит опять всё устанавливать и не факт, что что-нибудь не забудется и/или версия пакета не обновится. А с докером: установить докер, скачать проект и запустить подготовленную строку.


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


              1. apapacy
                03.01.2020 18:49

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


                1. t_kanstantsin
                  06.01.2020 18:59

                  Если в примере имидж — "мутный", то в примере без docker'a — у вас не будет никаких сведений (или очень поверхностных) о конфигурации сервера. Но это крайние случаи — и оба варианта одинаково неприятны. Тут причина не в docker'e, а в девопсе.
                  В моём опыте не крупных проектов с dockerfile'ами (без выделенного девопса) — они хранятся либо в самом проекте в папке docker, либо в отдельном репозитории той же компании. Это значительно упрощает настройку как локального окружения, так и сервера.


  1. apapacy
    02.01.2020 21:12
    +1

    Спасибо что обратили внимание на такую проблему. Я бы все же работал скорее с pm2 например как описано в статье https://en.programqa.com/question/52499715/ т.к. pm2 это (если не под cubernetes) наверное наилучший вариант.


    1. flancer Автор
      02.01.2020 22:45

      Спасибо за ссылку. Интересный workaround. Попробовал его применить на тестовом сервисе. Добавил в package.json:


        "scripts": {
          "safestart":"node --experimental-modules src/app_es6.mjs"
        }

      и запустил сервис командой:


      $ pm2 start npm  -- run safestart

      Видно, что ES6-приложение запускается:


      $ pm2 log
      ...
      /home/alex/.pm2/logs/npm-error.log last 15 lines:
      0|npm      | (node:11489) ExperimentalWarning: The ESM module loader is experimental.
      
      /home/alex/.pm2/logs/npm-out.log last 15 lines:
      0|npm      | 
      0|npm      | > pm2es6@0.1.0 safestart /home/alex/work/sof_es6_pm
      0|npm      | > node --experimental-modules src/app_es6.mjs
      0|npm      | 
      0|npm      | ES6 app listening on port 3000!
      0|npm      | Hello World! ES6 module is here.
      
      0|npm  | Hello World! ES6 module is here.
      0|npm  | Hello World! ES6 module is here.
      0|npm  | Hello World! ES6 module is here.

      К сожалению, нельзя использовать возможности кластеризации (-i 4) — запускается только первый инстанс приложения, остальные сваливаются. Но и то, что есть, гораздо интереснее выглядит, чем forever или systemd.


      1. monochromer
        03.01.2020 01:54
        +1

        Попробовал так (npm run cluster):


        {
          "type": "module",
          "scripts": {
            "start": "node --experimental-modules app.js",
            "cluster": "pm2 -i 4 start npm -- run start"
          },
        }

        Вроде запустилось:
        image


        1. flancer Автор
          03.01.2020 09:37
          +1

          Попробовал так же. Похоже, что запускается только один процесс, хотя pm2 думает, что запустились все:


          в pm2-логах также видны ошибки:


          $ pm2 log
          ...
          1|npm      | npm ERR! code ELIFECYCLE
          1|npm      | npm ERR! errno 1
          1|npm      | npm ERR! pm2es6@0.1.0 safestart: `node --experimental-modules src/app_es6.mjs`
          1|npm      | npm ERR! Exit status 1
          1|npm      | npm ERR! 
          1|npm      | npm ERR! Failed at the pm2es6@0.1.0 safestart script.
          1|npm      | npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

          В "нормальном" кластере запускается указанное кол-во процессов:


          alex@omen17:~/work/sof_es6_pm$ ps -Af | grep app_
          alex     10903 25382  2 08:29 ?        00:00:00 node /home/alex/work/sof_es6_pm/src/app_cjs.js
          alex     10910 25382  3 08:29 ?        00:00:00 node /home/alex/work/sof_es6_pm/src/app_cjs.js
          alex     10919 25382  2 08:29 ?        00:00:00 node /home/alex/work/sof_es6_pm/src/app_cjs.js
          alex     10932 25382  3 08:29 ?        00:00:00 node /home/alex/work/sof_es6_pm/src/app_cjs.js

          Думаю, что в моём тестовом приложении идёт конкуренция за порт 3000 для express-сервера. Какой-то из процессов первым захватывает порт, остальные отваливаются по ошибке. В логах это видно — при 4 экземплярах в кластере сообщений об ошибке только 3. pm2 каким-то образом обрабатывает вариант с портом самостоятельно, но как только мы стартуем приложение через node/npm, то pm2 теряет эту возможность.


  1. Kanumowa
    02.01.2020 23:45
    +3

    А почему запускали на 12ой ведь они там под флагом? Почему не попробовали тоже самое для 13.2.0+ без флага?


    1. flancer Автор
      03.01.2020 00:01

      12-я версия — LTS (Long Term Support). Только поэтому.


      1. justboris
        03.01.2020 10:26

        Long Term Support — это про стабильность. А модули пока ещё экспериментальные и их стабильность не гарантируется. Поэтому, в вашем случае от LTS пользы нет


        1. flancer Автор
          03.01.2020 11:32

          LTS — это наиболее вероятная версия софта. Я рассматривал частый кейс, а не последний. У меня и Ubuntu стоит 18.4 по той же причине — стабильность.


  1. Babayka_od
    03.01.2020 01:22

    Если хочется использовать es6 модули, то почему бы не воспользоваться бабалем?


    1. apapacy
      03.01.2020 02:29
      +1

      Бабелем я пользовался когда еs6 был еще практически без поддержки на нативном уровне. Сейчас когда все практически нативно бабелем нет резона пользоваться. Я даже в свое время отказался для этого от import в пользу require
      Сейчас я скорее предпочел бы не использовать import, чем использовать бабел.


  1. potorochinau
    03.01.2020 09:04

    Ещё бы рекомендовал использовать ecosystem.config.js в pm2. Про environments не забыть тоже с примерами.
    Давно пользуюсь pm2. Но вот тоже думаю пора в Докер. Ибо развёртывание приложений на новые машины уже утомляет, но я и не DevOps, просто FullStackJS. Пока не было задач, где бы я мог на 100% оправдать использование Докера. Чтобы выделить время на полное изучение его workflow.А очень хочется.


    1. VolCh
      03.01.2020 09:36

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


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


      1. flancer Автор
        03.01.2020 11:39

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


        1. VolCh
          04.01.2020 10:31

          Можно рассматривать базовый докерфайл и ко от разрабов как доку для девопса. Какие енв переменные приложение ожидает, какие конфиги, как его вообще собирать из исходников и как запускать. Есть и другие способы, конечно. И часть из них тоже отчасти самодокументируемые типа набора баш-скриптов, но плюс докера и подобных подходов (вагрант, например) — разработчик вынужден полностью описывать среду для своего приложения. Нет места (почти) "ой, забыл сказать, что нужно новое расширение для языка установить, я-то его ещё в первый день работы установил себе, наверное, но в проекте раньше не использовалось"


    1. apapacy
      03.01.2020 10:09

      Докер это очень просто, например гораздо проще чем bash. Нужно просто понять зачем он нужен. Но вот докер на проде это уже сложно. Т.к. без средств оркестрации на проде докер предоставит больше проблем чем преимуществ. А вот средства оркестрации это уже сложнее. Например cubernetes практически невозможно развернуть и главное поддерживать в рабочем состоянии не специалисту. Поэтому приходится покупать облачный. Поэтому его пиарят нещадно. Nomad реально развернуть и поддерживать самостоятельно но мало специалистов.


      1. VolCh
        04.01.2020 10:32

        Докер не проще чем баш, потому что без баша нормально с докером работать не получится обычно :)


  1. Viceroyalty
    03.01.2020 19:20

    Возможно я отстал от жизни или прилетел с другой планеты — но неужели JS настолько популярнее стандартных С/С++/С#, что он активно развивается на стороне сервера?


    1. apapacy
      03.01.2020 21:45

      С на стороне сервера используется для написание собственно серверов. Веб приложения как правило на с не пишут. Js используется чаще чем с. С# это как бы в основном для экосистемы windows. Для этой экосистемы с# скорее всего превалирует.


  1. Fi1osof
    03.01.2020 20:17

    Менеджер процессов, подобный pm2, является очень полезным сервисом в мире серверного JS. Но, к сожалению, сам pm2 не позволяет запускать современные nodejs-приложения (в частности — с ES6-модулями).

    Я сильно не изучал этот момент, но у меня вполне получается через pm2 запускать package.json-скрипт с передачей этого флага, и все работает норм.
    «scripts»: {
    «start-server»: «node --experimental-modules src/server/»,


    1. apapacy
      03.01.2020 21:42

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


      1. Fi1osof
        04.01.2020 00:45

        ОК. Буду знать. Хотя мне не доводилось еще сталкиваться с этой проблемой.