Blame SOURCES/bz1783106-01-fix-sinatra-wrapper-performance-issue.patch

d759a1
From d54c102cee7a61dd3eccd62d60af218aa97a85fc Mon Sep 17 00:00:00 2001
d759a1
From: Ivan Devat <idevat@redhat.com>
d759a1
Date: Thu, 9 Jan 2020 15:53:37 +0100
d759a1
Subject: [PATCH 6/7] squash bz1783106 fix-sinatra-wrapper-performance-issue
d759a1
d759a1
create prototype of tornado - thin communication
d759a1
d759a1
put socket path to settings
d759a1
d759a1
don't mix logs from threads in ruby daemon
d759a1
d759a1
run ruby daemon via systemd units
d759a1
d759a1
support trailing slash by gui urls e.g. /manage/
d759a1
d759a1
decode body from ruby response for log
d759a1
d759a1
configure ruby wrapper by socket path
d759a1
d759a1
remove env values not used for ruby calls any more
d759a1
d759a1
deal with ruby daemon communication issues
d759a1
d759a1
fix tests
d759a1
d759a1
cleanup ruby server code
d759a1
d759a1
deal with errors from ruby daemon in python daemon
d759a1
d759a1
remove unused cmdline wrapper
d759a1
d759a1
add ruby daemon infrastructure to spec etc.
d759a1
d759a1
stop logging to stderr from ruby daemon
d759a1
d759a1
fix spec file
d759a1
d759a1
* add missing cp for new rubygems
d759a1
* make sure to start the new ruby daemon on package upgrade
d759a1
* tests: give the new daemon enough time to start
d759a1
---
d759a1
 .gitlab-ci.yml                            |   7 +-
d759a1
 Makefile                                  |   6 +
d759a1
 pcs.spec.in                               |  30 ++++-
d759a1
 pcs/daemon/app/sinatra_ui.py              |   2 +-
d759a1
 pcs/daemon/env.py                         |  36 ------
d759a1
 pcs/daemon/ruby_pcsd.py                   | 136 +++++++++++-----------
d759a1
 pcs/daemon/run.py                         |   8 +-
d759a1
 pcs/settings_default.py                   |   1 +
d759a1
 pcs_test/tier0/daemon/app/fixtures_app.py |   3 +-
d759a1
 pcs_test/tier0/daemon/test_env.py         |  66 +----------
d759a1
 pcs_test/tier0/daemon/test_ruby_pcsd.py   |  13 +--
d759a1
 pcsd/Gemfile                              |   1 +
d759a1
 pcsd/Gemfile.lock                         |   7 ++
d759a1
 pcsd/Makefile                             |   3 +
d759a1
 pcsd/bootstrap.rb                         |  20 +++-
d759a1
 pcsd/cfgsync.rb                           |   6 +-
d759a1
 pcsd/pcs.rb                               |   9 +-
d759a1
 pcsd/pcsd-cli.rb                          |   3 +-
d759a1
 pcsd/pcsd-ruby.service                    |  20 ++++
d759a1
 pcsd/pcsd.conf                            |   4 +
d759a1
 pcsd/pcsd.rb                              |  31 ++---
d759a1
 pcsd/pcsd.service                         |   2 +
d759a1
 pcsd/pcsd.service-runner                  |  24 ++++
d759a1
 pcsd/remote.rb                            |   6 +-
d759a1
 pcsd/rserver.rb                           |  98 ++++++++++++++++
d759a1
 pcsd/settings.rb                          |   1 +
d759a1
 pcsd/settings.rb.debian                   |   1 +
d759a1
 pcsd/sinatra_cmdline_wrapper.rb           |  63 ----------
d759a1
 28 files changed, 330 insertions(+), 277 deletions(-)
d759a1
 create mode 100644 pcsd/pcsd-ruby.service
d759a1
 create mode 100644 pcsd/pcsd.service-runner
d759a1
 create mode 100644 pcsd/rserver.rb
d759a1
 delete mode 100644 pcsd/sinatra_cmdline_wrapper.rb
d759a1
d759a1
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
d759a1
index 23ab56a9..92b32033 100644
d759a1
--- a/.gitlab-ci.yml
d759a1
+++ b/.gitlab-ci.yml
d759a1
@@ -116,8 +116,11 @@ python_smoke_tests:
d759a1
         procps-ng
d759a1
         rpms/pcs-ci-*.rpm
d759a1
       "
d759a1
-    - /usr/sbin/pcsd & # start pcsd
d759a1
-    - sleep 10 # wait for pcsd to start up properly
d759a1
+    - export GEM_HOME=/usr/lib/pcsd/vendor/bundle/ruby
d759a1
+    - /usr/lib/pcsd/pcsd & # start pcsd (ruby - thin)
d759a1
+    - sleep 10 # wait for pcsd (ruby - thin) to start up properly
d759a1
+    - /usr/sbin/pcsd & # start pcsd (python - tornado)
d759a1
+    - sleep 10 # wait for pcsd (python - tornado) to start up properly
d759a1
     - pcs_test/smoke.sh
d759a1
   artifacts:
d759a1
     paths:
d759a1
diff --git a/Makefile b/Makefile
d759a1
index f2b0d9b9..b9f64acd 100644
d759a1
--- a/Makefile
d759a1
+++ b/Makefile
d759a1
@@ -267,7 +267,12 @@ ifeq ($(IS_DEBIAN)$(IS_SYSTEMCTL),truefalse)
d759a1
 else
d759a1
 	install -d ${DEST_SYSTEMD_SYSTEM}
d759a1
 	install -m 644 ${SYSTEMD_SERVICE_FILE} ${DEST_SYSTEMD_SYSTEM}/pcsd.service
d759a1
+	install -m 644 pcsd/pcsd-ruby.service ${DEST_SYSTEMD_SYSTEM}/pcsd-ruby.service
d759a1
 endif
d759a1
+	# ${DEST_LIB}/pcsd/pcsd holds the selinux context
d759a1
+	install -m 755 pcsd/pcsd.service-runner ${DEST_LIB}/pcsd/pcsd
d759a1
+	rm ${DEST_LIB}/pcsd/pcsd.service-runner
d759a1
+	
d759a1
 	install -m 700 -d ${DESTDIR}/var/lib/pcsd
d759a1
 	install -m 644 -D pcsd/pcsd.logrotate ${DESTDIR}/etc/logrotate.d/pcsd
d759a1
 	install -m644 -D pcsd/pcsd.8 ${DEST_MAN}/pcsd.8
d759a1
@@ -293,6 +298,7 @@ ifeq ($(IS_DEBIAN)$(IS_SYSTEMCTL),truefalse)
d759a1
 	rm -f ${DEST_INIT}/pcsd
d759a1
 else
d759a1
 	rm -f ${DEST_SYSTEMD_SYSTEM}/pcsd.service
d759a1
+	rm -f ${DEST_SYSTEMD_SYSTEM}/pcsd-ruby.service
d759a1
 	rm -f ${DEST_SYSTEMD_SYSTEM}/pcs_snmp_agent.service
d759a1
 endif
d759a1
 	rm -f ${DESTDIR}/etc/pam.d/pcsd
d759a1
diff --git a/pcs.spec.in b/pcs.spec.in
d759a1
index 5195dc51..32fbf614 100644
d759a1
--- a/pcs.spec.in
d759a1
+++ b/pcs.spec.in
d759a1
@@ -28,7 +28,9 @@ Summary: Pacemaker Configuration System
d759a1
 %global pyagentx_version   0.4.pcs.2
d759a1
 %global tornado_version    6.0.3
d759a1
 %global version_rubygem_backports  3.11.4
d759a1
+%global version_rubygem_daemons  1.3.1
d759a1
 %global version_rubygem_ethon  0.11.0
d759a1
+%global version_rubygem_eventmachine  1.2.7
d759a1
 %global version_rubygem_ffi  1.9.25
d759a1
 %global version_rubygem_json  2.1.0
d759a1
 %global version_rubygem_mustermann  1.0.3
d759a1
@@ -37,6 +39,7 @@ Summary: Pacemaker Configuration System
d759a1
 %global version_rubygem_rack_protection  2.0.4
d759a1
 %global version_rubygem_rack_test  1.0.0
d759a1
 %global version_rubygem_sinatra  2.0.4
d759a1
+%global version_rubygem_thin  1.7.2
d759a1
 %global version_rubygem_tilt  2.0.9
d759a1
 
d759a1
 # We do not use _libdir macro because upstream is not prepared for it.
d759a1
@@ -83,6 +86,9 @@ Source89: https://rubygems.org/downloads/rack-protection-%{version_rubygem_rack_
d759a1
 Source90: https://rubygems.org/downloads/rack-test-%{version_rubygem_rack_test}.gem
d759a1
 Source91: https://rubygems.org/downloads/sinatra-%{version_rubygem_sinatra}.gem
d759a1
 Source92: https://rubygems.org/downloads/tilt-%{version_rubygem_tilt}.gem
d759a1
+Source93: https://rubygems.org/downloads/eventmachine-%{version_rubygem_eventmachine}.gem
d759a1
+Source94: https://rubygems.org/downloads/daemons-%{version_rubygem_daemons}.gem
d759a1
+Source95: https://rubygems.org/downloads/thin-%{version_rubygem_thin}.gem
d759a1
 
d759a1
 Source100: https://github.com/idevat/pcs-web-ui/archive/%{ui_commit}/%{ui_src_name}.tar.gz
d759a1
 Source101: https://github.com/idevat/pcs-web-ui/releases/download/%{ui_commit}/pcs-web-ui-node-modules-%{ui_commit}.tar.xz
d759a1
@@ -164,7 +170,9 @@ Recommends: overpass-fonts
d759a1
 
d759a1
 Provides: bundled(tornado) = %{tornado_version}
d759a1
 Provides: bundled(backports) = %{version_rubygem_backports}
d759a1
+Provides: bundled(daemons) = %{version_rubygem_daemons}
d759a1
 Provides: bundled(ethon) = %{version_rubygem_ethon}
d759a1
+Provides: bundled(eventmachine) = %{version_rubygem_eventmachine}
d759a1
 Provides: bundled(ffi) = %{version_rubygem_ffi}
d759a1
 Provides: bundled(json) = %{version_rubygem_json}
d759a1
 Provides: bundled(mustermann) = %{version_rubygem_mustermann}
d759a1
@@ -173,6 +181,7 @@ Provides: bundled(rack) = %{version_rubygem_rack}
d759a1
 Provides: bundled(rack) = %{version_rubygem_rack_protection}
d759a1
 Provides: bundled(rack) = %{version_rubygem_rack_test}
d759a1
 Provides: bundled(sinatra) = %{version_rubygem_sinatra}
d759a1
+Provides: bundled(thin) = %{version_rubygem_thin}
d759a1
 Provides: bundled(tilt) = %{version_rubygem_tilt}
d759a1
 
d759a1
 %description
d759a1
@@ -228,6 +237,9 @@ cp -f %SOURCE89 pcsd/vendor/cache
d759a1
 cp -f %SOURCE90 pcsd/vendor/cache
d759a1
 cp -f %SOURCE91 pcsd/vendor/cache
d759a1
 cp -f %SOURCE92 pcsd/vendor/cache
d759a1
+cp -f %SOURCE93 pcsd/vendor/cache
d759a1
+cp -f %SOURCE94 pcsd/vendor/cache
d759a1
+cp -f %SOURCE95 pcsd/vendor/cache
d759a1
 
d759a1
 
d759a1
 # 3) dir for python bundles
d759a1
@@ -262,15 +274,18 @@ gem install \
d759a1
   --force --verbose -l --no-user-install %{gem_install_params} \
d759a1
   -i %{rubygem_bundle_dir} \
d759a1
   %{rubygem_cache_dir}/backports-%{version_rubygem_backports}.gem \
d759a1
+  %{rubygem_cache_dir}/daemons-%{version_rubygem_daemons}.gem \
d759a1
   %{rubygem_cache_dir}/ethon-%{version_rubygem_ethon}.gem \
d759a1
+  %{rubygem_cache_dir}/eventmachine-%{version_rubygem_eventmachine}.gem \
d759a1
   %{rubygem_cache_dir}/ffi-%{version_rubygem_ffi}.gem \
d759a1
   %{rubygem_cache_dir}/json-%{version_rubygem_json}.gem \
d759a1
   %{rubygem_cache_dir}/mustermann-%{version_rubygem_mustermann}.gem \
d759a1
   %{rubygem_cache_dir}/open4-%{version_rubygem_open4}.gem \
d759a1
-  %{rubygem_cache_dir}/rack-%{version_rubygem_rack}.gem \
d759a1
   %{rubygem_cache_dir}/rack-protection-%{version_rubygem_rack_protection}.gem \
d759a1
   %{rubygem_cache_dir}/rack-test-%{version_rubygem_rack_test}.gem \
d759a1
+  %{rubygem_cache_dir}/rack-%{version_rubygem_rack}.gem \
d759a1
   %{rubygem_cache_dir}/sinatra-%{version_rubygem_sinatra}.gem \
d759a1
+  %{rubygem_cache_dir}/thin-%{version_rubygem_thin}.gem \
d759a1
   %{rubygem_cache_dir}/tilt-%{version_rubygem_tilt}.gem \
d759a1
   -- '--with-ldflags=-Wl,-z,relro -Wl,-z,ibt -Wl,-z,now -Wl,--gc-sections' \
d759a1
      '--with-cflags=-O2 -ffunction-sections'
d759a1
@@ -324,20 +339,31 @@ rm -r -v ${pcsd_dir}/test
d759a1
 # remove javascript testing files
d759a1
 rm -r -v ${pcsd_dir}/public/js/dev
d759a1
 
d759a1
+%posttrans
d759a1
+# Make sure the new version of the daemon is runnning.
d759a1
+# Also, make sure to start pcsd-ruby if it hasn't been started or even
d759a1
+# installed before. This is done by restarting pcsd.service.
d759a1
+%{_bindir}/systemctl daemon-reload
d759a1
+%{_bindir}/systemctl try-restart pcsd.service
d759a1
+
d759a1
+
d759a1
 %post
d759a1
 %systemd_post pcsd.service
d759a1
+%systemd_post pcsd-ruby.service
d759a1
 
d759a1
 %post -n %{pcs_snmp_pkg_name}
d759a1
 %systemd_post pcs_snmp_agent.service
d759a1
 
d759a1
 %preun
d759a1
 %systemd_preun pcsd.service
d759a1
+%systemd_preun pcsd-ruby.service
d759a1
 
d759a1
 %preun -n %{pcs_snmp_pkg_name}
d759a1
 %systemd_preun pcs_snmp_agent.service
d759a1
 
d759a1
 %postun
d759a1
 %systemd_postun_with_restart pcsd.service
d759a1
+%systemd_postun_with_restart pcsd-ruby.service
d759a1
 
d759a1
 %postun -n %{pcs_snmp_pkg_name}
d759a1
 %systemd_postun_with_restart pcs_snmp_agent.service
d759a1
@@ -357,6 +383,7 @@ rm -r -v ${pcsd_dir}/public/js/dev
d759a1
 %{pcs_libdir}/pcsd/.bundle/config
d759a1
 %{pcs_libdir}/pcs/bundled/packages/tornado*
d759a1
 %{_unitdir}/pcsd.service
d759a1
+%{_unitdir}/pcsd-ruby.service
d759a1
 %{_datadir}/bash-completion/completions/pcs
d759a1
 %{_sharedstatedir}/pcsd
d759a1
 %{_sysconfdir}/pam.d/pcsd
d759a1
@@ -374,6 +401,7 @@ rm -r -v ${pcsd_dir}/public/js/dev
d759a1
 %{_mandir}/man8/pcsd.*
d759a1
 %exclude %{pcs_libdir}/pcsd/*.debian
d759a1
 %exclude %{pcs_libdir}/pcsd/pcsd.service
d759a1
+%exclude %{pcs_libdir}/pcsd/pcsd-ruby.service
d759a1
 %exclude %{pcs_libdir}/pcsd/pcsd.conf
d759a1
 %exclude %{pcs_libdir}/pcsd/pcsd.8
d759a1
 %exclude %{pcs_libdir}/pcsd/public/js/dev/*
d759a1
diff --git a/pcs/daemon/app/sinatra_ui.py b/pcs/daemon/app/sinatra_ui.py
d759a1
index 1348134d..5315a48f 100644
d759a1
--- a/pcs/daemon/app/sinatra_ui.py
d759a1
+++ b/pcs/daemon/app/sinatra_ui.py
d759a1
@@ -153,7 +153,7 @@ def get_routes(
d759a1
         # The protection by session was moved from ruby code to python code
d759a1
         # (tornado).
d759a1
         (
d759a1
-            r"/($|manage$|permissions$|managec/.+/main)",
d759a1
+            r"/($|manage/?$|permissions/?$|managec/.+/main)",
d759a1
             SinatraGuiProtected,
d759a1
             {**sessions, **ruby_wrapper}
d759a1
         ),
d759a1
diff --git a/pcs/daemon/env.py b/pcs/daemon/env.py
d759a1
index 54a9819f..26cdcf9b 100644
d759a1
--- a/pcs/daemon/env.py
d759a1
+++ b/pcs/daemon/env.py
d759a1
@@ -15,7 +15,6 @@ from pcs.lib.validate import is_port_number
d759a1
 # Relative location instead of system location is used for development purposes.
d759a1
 PCSD_LOCAL_DIR = realpath(dirname(abspath(__file__)) + "/../../pcsd")
d759a1
 
d759a1
-PCSD_CMDLINE_ENTRY_RB_SCRIPT = "sinatra_cmdline_wrapper.rb"
d759a1
 PCSD_STATIC_FILES_DIR_NAME = "public"
d759a1
 
d759a1
 PCSD_PORT = "PCSD_PORT"
d759a1
@@ -26,12 +25,8 @@ NOTIFY_SOCKET = "NOTIFY_SOCKET"
d759a1
 PCSD_DEBUG = "PCSD_DEBUG"
d759a1
 PCSD_DISABLE_GUI = "PCSD_DISABLE_GUI"
d759a1
 PCSD_SESSION_LIFETIME = "PCSD_SESSION_LIFETIME"
d759a1
-GEM_HOME = "GEM_HOME"
d759a1
 PCSD_DEV = "PCSD_DEV"
d759a1
-PCSD_CMDLINE_ENTRY = "PCSD_CMDLINE_ENTRY"
d759a1
 PCSD_STATIC_FILES_DIR = "PCSD_STATIC_FILES_DIR"
d759a1
-HTTPS_PROXY = "HTTPS_PROXY"
d759a1
-NO_PROXY = "NO_PROXY"
d759a1
 
d759a1
 Env = namedtuple("Env", [
d759a1
     PCSD_PORT,
d759a1
@@ -42,11 +37,7 @@ Env = namedtuple("Env", [
d759a1
     PCSD_DEBUG,
d759a1
     PCSD_DISABLE_GUI,
d759a1
     PCSD_SESSION_LIFETIME,
d759a1
-    GEM_HOME,
d759a1
-    PCSD_CMDLINE_ENTRY,
d759a1
     PCSD_STATIC_FILES_DIR,
d759a1
-    HTTPS_PROXY,
d759a1
-    NO_PROXY,
d759a1
     PCSD_DEV,
d759a1
     "has_errors",
d759a1
 ])
d759a1
@@ -62,11 +53,7 @@ def prepare_env(environ, logger=None):
d759a1
         loader.pcsd_debug(),
d759a1
         loader.pcsd_disable_gui(),
d759a1
         loader.session_lifetime(),
d759a1
-        loader.gem_home(),
d759a1
-        loader.pcsd_cmdline_entry(),
d759a1
         loader.pcsd_static_files_dir(),
d759a1
-        loader.https_proxy(),
d759a1
-        loader.no_proxy(),
d759a1
         loader.pcsd_dev(),
d759a1
         loader.has_errors(),
d759a1
     )
d759a1
@@ -173,20 +160,6 @@ class EnvLoader:
d759a1
     def pcsd_debug(self):
d759a1
         return self.__has_true_in_environ(PCSD_DEBUG)
d759a1
 
d759a1
-    def gem_home(self):
d759a1
-        if settings.pcsd_gem_path is None:
d759a1
-            return None
d759a1
-        return self.__in_pcsd_path(
d759a1
-            settings.pcsd_gem_path,
d759a1
-            "Ruby gem location"
d759a1
-        )
d759a1
-
d759a1
-    def pcsd_cmdline_entry(self):
d759a1
-        return self.__in_pcsd_path(
d759a1
-            PCSD_CMDLINE_ENTRY_RB_SCRIPT,
d759a1
-            "Ruby handlers entrypoint"
d759a1
-        )
d759a1
-
d759a1
     def pcsd_static_files_dir(self):
d759a1
         return self.__in_pcsd_path(
d759a1
             PCSD_STATIC_FILES_DIR_NAME,
d759a1
@@ -194,15 +167,6 @@ class EnvLoader:
d759a1
             existence_required=not self.pcsd_disable_gui()
d759a1
         )
d759a1
 
d759a1
-    def https_proxy(self):
d759a1
-        for key in ["https_proxy", HTTPS_PROXY, "all_proxy", "ALL_PROXY"]:
d759a1
-            if key in self.environ:
d759a1
-                return self.environ[key]
d759a1
-        return None
d759a1
-
d759a1
-    def no_proxy(self):
d759a1
-        return self.environ.get("no_proxy", self.environ.get(NO_PROXY, None))
d759a1
-
d759a1
     @lru_cache()
d759a1
     def pcsd_dev(self):
d759a1
         return self.__has_true_in_environ(PCSD_DEV)
d759a1
diff --git a/pcs/daemon/ruby_pcsd.py b/pcs/daemon/ruby_pcsd.py
d759a1
index 5bdaffeb..e612f8da 100644
d759a1
--- a/pcs/daemon/ruby_pcsd.py
d759a1
+++ b/pcs/daemon/ruby_pcsd.py
d759a1
@@ -1,14 +1,16 @@
d759a1
 import json
d759a1
 import logging
d759a1
-import os.path
d759a1
-from base64 import b64decode
d759a1
+from base64 import b64decode, b64encode, binascii
d759a1
 from collections import namedtuple
d759a1
 from time import time as now
d759a1
 
d759a1
-from tornado.gen import multi, convert_yielded
d759a1
+import pycurl
d759a1
+from tornado.gen import convert_yielded
d759a1
 from tornado.web import HTTPError
d759a1
 from tornado.httputil import split_host_and_port, HTTPServerRequest
d759a1
-from tornado.process import Subprocess
d759a1
+from tornado.httpclient import AsyncHTTPClient
d759a1
+from tornado.curl_httpclient import CurlError
d759a1
+
d759a1
 
d759a1
 from pcs.daemon import log
d759a1
 
d759a1
@@ -33,7 +35,7 @@ class SinatraResult(namedtuple("SinatraResult", "headers, status, body")):
d759a1
         return cls(
d759a1
             response["headers"],
d759a1
             response["status"],
d759a1
-            b64decode(response["body"])
d759a1
+            response["body"]
d759a1
         )
d759a1
 
d759a1
 def log_group_id_generator():
d759a1
@@ -58,24 +60,12 @@ def process_response_logs(rb_log_list):
d759a1
             group_id=group_id
d759a1
         )
d759a1
 
d759a1
-def log_communication(request_json, stdout, stderr):
d759a1
-    log.pcsd.debug("Request for ruby pcsd wrapper: '%s'", request_json)
d759a1
-    log.pcsd.debug("Response stdout from ruby pcsd wrapper: '%s'", stdout)
d759a1
-    log.pcsd.debug("Response stderr from ruby pcsd wrapper: '%s'", stderr)
d759a1
-
d759a1
 class Wrapper:
d759a1
-    # pylint: disable=too-many-instance-attributes
d759a1
-    def __init__(
d759a1
-        self, pcsd_cmdline_entry, gem_home=None, debug=False,
d759a1
-        ruby_executable="ruby", https_proxy=None, no_proxy=None
d759a1
-    ):
d759a1
-        self.__gem_home = gem_home
d759a1
-        self.__pcsd_cmdline_entry = pcsd_cmdline_entry
d759a1
-        self.__pcsd_dir = os.path.dirname(pcsd_cmdline_entry)
d759a1
-        self.__ruby_executable = ruby_executable
d759a1
+    def __init__(self, pcsd_ruby_socket, debug=False):
d759a1
         self.__debug = debug
d759a1
-        self.__https_proxy = https_proxy
d759a1
-        self.__no_proxy = no_proxy
d759a1
+        AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient')
d759a1
+        self.__client = AsyncHTTPClient()
d759a1
+        self.__pcsd_ruby_socket = pcsd_ruby_socket
d759a1
 
d759a1
     @staticmethod
d759a1
     def get_sinatra_request(request: HTTPServerRequest):
d759a1
@@ -102,55 +92,76 @@ class Wrapper:
d759a1
             "rack.input": request.body.decode("utf8"),
d759a1
         }}
d759a1
 
d759a1
+    def prepare_curl_callback(self, curl):
d759a1
+        curl.setopt(pycurl.UNIX_SOCKET_PATH, self.__pcsd_ruby_socket)
d759a1
+        curl.setopt(pycurl.TIMEOUT, 70)
d759a1
+
d759a1
     async def send_to_ruby(self, request_json):
d759a1
-        env = {
d759a1
-            "PCSD_DEBUG": "true" if self.__debug else "false"
d759a1
-        }
d759a1
-        if self.__gem_home is not None:
d759a1
-            env["GEM_HOME"] = self.__gem_home
d759a1
-
d759a1
-        if self.__no_proxy is not None:
d759a1
-            env["NO_PROXY"] = self.__no_proxy
d759a1
-        if self.__https_proxy is not None:
d759a1
-            env["HTTPS_PROXY"] = self.__https_proxy
d759a1
-
d759a1
-        pcsd_ruby = Subprocess(
d759a1
-            [
d759a1
-                self.__ruby_executable, "-I",
d759a1
-                self.__pcsd_dir,
d759a1
-                self.__pcsd_cmdline_entry
d759a1
-            ],
d759a1
-            stdin=Subprocess.STREAM,
d759a1
-            stdout=Subprocess.STREAM,
d759a1
-            stderr=Subprocess.STREAM,
d759a1
-            env=env
d759a1
-        )
d759a1
-        await pcsd_ruby.stdin.write(str.encode(request_json))
d759a1
-        pcsd_ruby.stdin.close()
d759a1
-        return await multi([
d759a1
-            pcsd_ruby.stdout.read_until_close(),
d759a1
-            pcsd_ruby.stderr.read_until_close(),
d759a1
-            pcsd_ruby.wait_for_exit(raise_error=False),
d759a1
-        ])
d759a1
+        # We do not need location for communication with ruby itself since we
d759a1
+        # communicate via unix socket. But it is required by AsyncHTTPClient so
d759a1
+        # "localhost" is used.
d759a1
+        tornado_request = b64encode(request_json.encode()).decode()
d759a1
+        return (await self.__client.fetch(
d759a1
+            "localhost",
d759a1
+            method="POST",
d759a1
+            body=f"TORNADO_REQUEST={tornado_request}",
d759a1
+            prepare_curl_callback=self.prepare_curl_callback,
d759a1
+        )).body
d759a1
 
d759a1
     async def run_ruby(self, request_type, request=None):
d759a1
+        """
d759a1
+        request_type: SINATRA_GUI|SINATRA_REMOTE|SYNC_CONFIGS
d759a1
+        request: result of get_sinatra_request|None
d759a1
+            i.e. it has structure returned by get_sinatra_request if the request
d759a1
+            is not None - so we can get SERVER_NAME and  SERVER_PORT
d759a1
+        """
d759a1
         request = request or {}
d759a1
         request.update({"type": request_type})
d759a1
         request_json = json.dumps(request)
d759a1
-        stdout, stderr, dummy_status = await self.send_to_ruby(request_json)
d759a1
+
d759a1
+        if self.__debug:
d759a1
+            log.pcsd.debug("Ruby daemon request: '%s'", request_json)
d759a1
         try:
d759a1
-            response = json.loads(stdout)
d759a1
-        except json.JSONDecodeError as e:
d759a1
-            self.__log_bad_response(
d759a1
-                f"Cannot decode json from ruby pcsd wrapper: '{e}'",
d759a1
-                request_json, stdout, stderr
d759a1
+            ruby_response = await self.send_to_ruby(request_json)
d759a1
+        except CurlError as e:
d759a1
+            log.pcsd.error(
d759a1
+                "Cannot connect to ruby daemon (message: '%s'). Is it running?",
d759a1
+                e
d759a1
             )
d759a1
             raise HTTPError(500)
d759a1
-        else:
d759a1
-            if self.__debug:
d759a1
-                log_communication(request_json, stdout, stderr)
d759a1
-            process_response_logs(response["logs"])
d759a1
+
d759a1
+        try:
d759a1
+            response = json.loads(ruby_response)
d759a1
+            if "error" in response:
d759a1
+                log.pcsd.error(
d759a1
+                    "Ruby daemon response contains an error: '%s'",
d759a1
+                    json.dumps(response)
d759a1
+                )
d759a1
+                raise HTTPError(500)
d759a1
+
d759a1
+            logs = response.pop("logs", [])
d759a1
+            if "body" in response:
d759a1
+                body = b64decode(response.pop("body"))
d759a1
+                if self.__debug:
d759a1
+                    log.pcsd.debug(
d759a1
+                        "Ruby daemon response (without logs and body): '%s'",
d759a1
+                        json.dumps(response)
d759a1
+                    )
d759a1
+                    log.pcsd.debug("Ruby daemon response body: '%s'", body)
d759a1
+                response["body"] = body
d759a1
+
d759a1
+            elif self.__debug:
d759a1
+                log.pcsd.debug(
d759a1
+                    "Ruby daemon response (without logs): '%s'",
d759a1
+                    json.dumps(response)
d759a1
+                )
d759a1
+            process_response_logs(logs)
d759a1
             return response
d759a1
+        except (json.JSONDecodeError, binascii.Error) as e:
d759a1
+            if self.__debug:
d759a1
+                log.pcsd.debug("Ruby daemon response: '%s'", ruby_response)
d759a1
+            log.pcsd.error("Cannot decode json from ruby pcsd wrapper: '%s'", e)
d759a1
+            raise HTTPError(500)
d759a1
 
d759a1
     async def request_gui(
d759a1
         self, request: HTTPServerRequest, user, groups, is_authenticated
d759a1
@@ -186,8 +197,3 @@ class Wrapper:
d759a1
         except HTTPError:
d759a1
             log.pcsd.error("Config synchronization failed")
d759a1
             return int(now()) + DEFAULT_SYNC_CONFIG_DELAY
d759a1
-
d759a1
-    def __log_bad_response(self, error_message, request_json, stdout, stderr):
d759a1
-        log.pcsd.error(error_message)
d759a1
-        if self.__debug:
d759a1
-            log_communication(request_json, stdout, stderr)
d759a1
diff --git a/pcs/daemon/run.py b/pcs/daemon/run.py
d759a1
index bafd9f3c..874ee2f1 100644
d759a1
--- a/pcs/daemon/run.py
d759a1
+++ b/pcs/daemon/run.py
d759a1
@@ -65,6 +65,8 @@ def configure_app(
d759a1
                 # old web ui by default
d759a1
                 [(r"/", RedirectHandler, dict(url="/manage"))]
d759a1
                 +
d759a1
+                [(r"/ui", RedirectHandler, dict(url="/ui/"))]
d759a1
+                +
d759a1
                 ui.get_routes(
d759a1
                     url_prefix="/ui/",
d759a1
                     app_dir=os.path.join(public_dir, "ui"),
d759a1
@@ -101,12 +103,8 @@ def main():
d759a1
 
d759a1
     sync_config_lock = Lock()
d759a1
     ruby_pcsd_wrapper = ruby_pcsd.Wrapper(
d759a1
-        pcsd_cmdline_entry=env.PCSD_CMDLINE_ENTRY,
d759a1
-        gem_home=env.GEM_HOME,
d759a1
+        settings.pcsd_ruby_socket,
d759a1
         debug=env.PCSD_DEBUG,
d759a1
-        ruby_executable=settings.ruby_executable,
d759a1
-        https_proxy=env.HTTPS_PROXY,
d759a1
-        no_proxy=env.NO_PROXY,
d759a1
     )
d759a1
     make_app = configure_app(
d759a1
         session.Storage(env.PCSD_SESSION_LIFETIME),
d759a1
diff --git a/pcs/settings_default.py b/pcs/settings_default.py
d759a1
index 6d8f33ac..f761ce43 100644
d759a1
--- a/pcs/settings_default.py
d759a1
+++ b/pcs/settings_default.py
d759a1
@@ -43,6 +43,7 @@ cibadmin = os.path.join(pacemaker_binaries, "cibadmin")
d759a1
 crm_mon_schema = '/usr/share/pacemaker/crm_mon.rng'
d759a1
 agent_metadata_schema = "/usr/share/resource-agents/ra-api-1.dtd"
d759a1
 pcsd_var_location = "/var/lib/pcsd/"
d759a1
+pcsd_ruby_socket = "/run/pcsd-ruby.socket"
d759a1
 pcsd_cert_location = os.path.join(pcsd_var_location, "pcsd.crt")
d759a1
 pcsd_key_location = os.path.join(pcsd_var_location, "pcsd.key")
d759a1
 pcsd_known_hosts_location = os.path.join(pcsd_var_location, "known-hosts")
d759a1
diff --git a/pcs_test/tier0/daemon/app/fixtures_app.py b/pcs_test/tier0/daemon/app/fixtures_app.py
d759a1
index 2e4feba4..8d5b8f4c 100644
d759a1
--- a/pcs_test/tier0/daemon/app/fixtures_app.py
d759a1
+++ b/pcs_test/tier0/daemon/app/fixtures_app.py
d759a1
@@ -1,4 +1,3 @@
d759a1
-from base64 import b64encode
d759a1
 from pprint import pformat
d759a1
 from urllib.parse import urlencode
d759a1
 
d759a1
@@ -30,7 +29,7 @@ class RubyPcsdWrapper(ruby_pcsd.Wrapper):
d759a1
         return {
d759a1
             "headers": self.headers,
d759a1
             "status": self.status_code,
d759a1
-            "body": b64encode(self.body),
d759a1
+            "body": self.body,
d759a1
         }
d759a1
 
d759a1
 class AppTest(AsyncHTTPTestCase):
d759a1
diff --git a/pcs_test/tier0/daemon/test_env.py b/pcs_test/tier0/daemon/test_env.py
d759a1
index 9e78eafd..e2f7f5b1 100644
d759a1
--- a/pcs_test/tier0/daemon/test_env.py
d759a1
+++ b/pcs_test/tier0/daemon/test_env.py
d759a1
@@ -41,11 +41,7 @@ class Prepare(TestCase, create_setup_patch_mixin(env)):
d759a1
             env.PCSD_DEBUG: False,
d759a1
             env.PCSD_DISABLE_GUI: False,
d759a1
             env.PCSD_SESSION_LIFETIME: settings.gui_session_lifetime_seconds,
d759a1
-            env.GEM_HOME: pcsd_dir(settings.pcsd_gem_path),
d759a1
-            env.PCSD_CMDLINE_ENTRY: pcsd_dir(env.PCSD_CMDLINE_ENTRY_RB_SCRIPT),
d759a1
             env.PCSD_STATIC_FILES_DIR: pcsd_dir(env.PCSD_STATIC_FILES_DIR_NAME),
d759a1
-            env.HTTPS_PROXY: None,
d759a1
-            env.NO_PROXY: None,
d759a1
             env.PCSD_DEV: False,
d759a1
             "has_errors": False,
d759a1
         }
d759a1
@@ -77,8 +73,6 @@ class Prepare(TestCase, create_setup_patch_mixin(env)):
d759a1
             env.PCSD_DISABLE_GUI: "true",
d759a1
             env.PCSD_SESSION_LIFETIME: str(session_lifetime),
d759a1
             env.PCSD_DEV: "true",
d759a1
-            env.HTTPS_PROXY: "proxy1",
d759a1
-            env.NO_PROXY: "host",
d759a1
             env.PCSD_DEV: "true",
d759a1
         }
d759a1
         self.assert_environ_produces_modified_pcsd_env(
d759a1
@@ -92,15 +86,9 @@ class Prepare(TestCase, create_setup_patch_mixin(env)):
d759a1
                 env.PCSD_DEBUG: True,
d759a1
                 env.PCSD_DISABLE_GUI: True,
d759a1
                 env.PCSD_SESSION_LIFETIME: session_lifetime,
d759a1
-                env.GEM_HOME: pcsd_dir(settings.pcsd_gem_path),
d759a1
-                env.PCSD_CMDLINE_ENTRY: pcsd_dir(
d759a1
-                    env.PCSD_CMDLINE_ENTRY_RB_SCRIPT
d759a1
-                ),
d759a1
                 env.PCSD_STATIC_FILES_DIR: pcsd_dir(
d759a1
                     env.PCSD_STATIC_FILES_DIR_NAME
d759a1
                 ),
d759a1
-                env.HTTPS_PROXY: environ[env.HTTPS_PROXY],
d759a1
-                env.NO_PROXY: environ[env.NO_PROXY],
d759a1
                 env.PCSD_DEV: True,
d759a1
             },
d759a1
         )
d759a1
@@ -167,13 +155,6 @@ class Prepare(TestCase, create_setup_patch_mixin(env)):
d759a1
         self.assert_environ_produces_modified_pcsd_env(
d759a1
             specific_env_values={"has_errors": True},
d759a1
             errors=[
d759a1
-                f"Ruby gem location '{pcsd_dir(settings.pcsd_gem_path)}'"
d759a1
-                    " does not exist"
d759a1
-                ,
d759a1
-                "Ruby handlers entrypoint"
d759a1
-                    f" '{pcsd_dir(env.PCSD_CMDLINE_ENTRY_RB_SCRIPT)}'"
d759a1
-                    " does not exist"
d759a1
-                ,
d759a1
                 "Directory with web UI assets"
d759a1
                     f" '{pcsd_dir(env.PCSD_STATIC_FILES_DIR_NAME)}'"
d759a1
                     " does not exist"
d759a1
@@ -181,54 +162,13 @@ class Prepare(TestCase, create_setup_patch_mixin(env)):
d759a1
             ]
d759a1
         )
d759a1
 
d759a1
-    def test_errors_on_missing_paths_disabled_gui(self):
d759a1
+    def test_no_errors_on_missing_paths_disabled_gui(self):
d759a1
         self.path_exists.return_value = False
d759a1
-        pcsd_dir = partial(join_path, settings.pcsd_exec_location)
d759a1
         self.assert_environ_produces_modified_pcsd_env(
d759a1
             environ={env.PCSD_DISABLE_GUI: "true"},
d759a1
             specific_env_values={
d759a1
                 env.PCSD_DISABLE_GUI: True,
d759a1
-                "has_errors": True,
d759a1
+                "has_errors": False,
d759a1
             },
d759a1
-            errors=[
d759a1
-                f"Ruby gem location '{pcsd_dir(settings.pcsd_gem_path)}'"
d759a1
-                    " does not exist"
d759a1
-                ,
d759a1
-                "Ruby handlers entrypoint"
d759a1
-                    f" '{pcsd_dir(env.PCSD_CMDLINE_ENTRY_RB_SCRIPT)}'"
d759a1
-                    " does not exist"
d759a1
-                ,
d759a1
-            ]
d759a1
+            errors=[]
d759a1
         )
d759a1
-
d759a1
-    def test_lower_case_no_proxy_has_precedence(self):
d759a1
-        def it_selects(proxy_value):
d759a1
-            self.assert_environ_produces_modified_pcsd_env(
d759a1
-                environ=environ,
d759a1
-                specific_env_values={env.NO_PROXY: proxy_value}
d759a1
-            )
d759a1
-
d759a1
-        environ = {"NO_PROXY": "no_proxy_1"}
d759a1
-        it_selects("no_proxy_1")
d759a1
-
d759a1
-        environ["no_proxy"] = "no_proxy_2"
d759a1
-        it_selects("no_proxy_2")
d759a1
-
d759a1
-    def test_http_proxy_is_setup_by_precedence(self):
d759a1
-        def it_selects(proxy_value):
d759a1
-            self.assert_environ_produces_modified_pcsd_env(
d759a1
-                environ=environ,
d759a1
-                specific_env_values={env.HTTPS_PROXY: proxy_value}
d759a1
-            )
d759a1
-
d759a1
-        environ = {"ALL_PROXY": "all_proxy_1"}
d759a1
-        it_selects("all_proxy_1")
d759a1
-
d759a1
-        environ["all_proxy"] = "all_proxy_2"
d759a1
-        it_selects("all_proxy_2")
d759a1
-
d759a1
-        environ["HTTPS_PROXY"] = "https_proxy_1"
d759a1
-        it_selects("https_proxy_1")
d759a1
-
d759a1
-        environ["https_proxy"] = "https_proxy_2"
d759a1
-        it_selects("https_proxy_2")
d759a1
diff --git a/pcs_test/tier0/daemon/test_ruby_pcsd.py b/pcs_test/tier0/daemon/test_ruby_pcsd.py
d759a1
index d7fd71a0..28f14c87 100644
d759a1
--- a/pcs_test/tier0/daemon/test_ruby_pcsd.py
d759a1
+++ b/pcs_test/tier0/daemon/test_ruby_pcsd.py
d759a1
@@ -16,10 +16,7 @@ from pcs.daemon import ruby_pcsd
d759a1
 logging.getLogger("pcs.daemon").setLevel(logging.CRITICAL)
d759a1
 
d759a1
 def create_wrapper():
d759a1
-    return ruby_pcsd.Wrapper(
d759a1
-        rc("/path/to/gem_home"),
d759a1
-        rc("/path/to/pcsd/cmdline/entry"),
d759a1
-    )
d759a1
+    return ruby_pcsd.Wrapper(rc("/path/to/ruby_socket"))
d759a1
 
d759a1
 def create_http_request():
d759a1
     return HTTPServerRequest(
d759a1
@@ -63,9 +60,7 @@ patch_ruby_pcsd = create_patcher(ruby_pcsd)
d759a1
 
d759a1
 class RunRuby(AsyncTestCase):
d759a1
     def setUp(self):
d759a1
-        self.stdout = ""
d759a1
-        self.stderr = ""
d759a1
-        self.exit_status = 0
d759a1
+        self.ruby_response = ""
d759a1
         self.request = self.create_request()
d759a1
         self.wrapper = create_wrapper()
d759a1
         patcher = mock.patch.object(
d759a1
@@ -79,14 +74,14 @@ class RunRuby(AsyncTestCase):
d759a1
 
d759a1
     async def send_to_ruby(self, request_json):
d759a1
         self.assertEqual(json.loads(request_json), self.request)
d759a1
-        return self.stdout, self.stderr, self.exit_status
d759a1
+        return self.ruby_response
d759a1
 
d759a1
     @staticmethod
d759a1
     def create_request(_type=ruby_pcsd.SYNC_CONFIGS):
d759a1
         return {"type": _type}
d759a1
 
d759a1
     def set_run_result(self, run_result):
d759a1
-        self.stdout = json.dumps({**run_result, "logs": []})
d759a1
+        self.ruby_response = json.dumps({**run_result, "logs": []})
d759a1
 
d759a1
     def assert_sinatra_result(self, result, headers, status, body):
d759a1
         self.assertEqual(result.headers, headers)
d759a1
diff --git a/pcsd/Gemfile b/pcsd/Gemfile
d759a1
index 27898f71..716991a6 100644
d759a1
--- a/pcsd/Gemfile
d759a1
+++ b/pcsd/Gemfile
d759a1
@@ -10,3 +10,4 @@ gem 'json'
d759a1
 gem 'open4'
d759a1
 gem 'ffi'
d759a1
 gem 'ethon'
d759a1
+gem 'thin'
d759a1
diff --git a/pcsd/Gemfile.lock b/pcsd/Gemfile.lock
d759a1
index 6f833888..c8b02a94 100644
d759a1
--- a/pcsd/Gemfile.lock
d759a1
+++ b/pcsd/Gemfile.lock
d759a1
@@ -2,8 +2,10 @@ GEM
d759a1
   remote: https://rubygems.org/
d759a1
   specs:
d759a1
     backports (3.11.4)
d759a1
+    daemons (1.3.1)
d759a1
     ethon (0.11.0)
d759a1
       ffi (>= 1.3.0)
d759a1
+    eventmachine (1.2.7)
d759a1
     ffi (1.9.25)
d759a1
     json (2.1.0)
d759a1
     mustermann (1.0.3)
d759a1
@@ -18,6 +20,10 @@ GEM
d759a1
       rack (~> 2.0)
d759a1
       rack-protection (= 2.0.4)
d759a1
       tilt (~> 2.0)
d759a1
+    thin (1.7.2)
d759a1
+      daemons (~> 1.0, >= 1.0.9)
d759a1
+      eventmachine (~> 1.0, >= 1.0.4)
d759a1
+      rack (>= 1, < 3)
d759a1
     tilt (2.0.9)
d759a1
 
d759a1
 PLATFORMS
d759a1
@@ -33,6 +39,7 @@ DEPENDENCIES
d759a1
   rack-protection
d759a1
   rack-test
d759a1
   sinatra
d759a1
+  thin
d759a1
   tilt
d759a1
 
d759a1
 BUNDLED WITH
d759a1
diff --git a/pcsd/Makefile b/pcsd/Makefile
d759a1
index 5fe3f3f3..5dde50e3 100644
d759a1
--- a/pcsd/Makefile
d759a1
+++ b/pcsd/Makefile
d759a1
@@ -26,6 +26,9 @@ build_gems_without_bundler:
d759a1
 	vendor/cache/rack-test-1.1.0.gem \
d759a1
 	vendor/cache/sinatra-2.0.4.gem \
d759a1
 	vendor/cache/tilt-2.0.9.gem \
d759a1
+	vendor/cache/eventmachine-1.2.7.gem \
d759a1
+	vendor/cache/daemons-1.3.1.gem \
d759a1
+	vendor/cache/thin-1.7.2.gem \
d759a1
 	-- '--with-ldflags="-Wl,-z,now -Wl,-z,relro"'
d759a1
 
d759a1
 get_gems:
d759a1
diff --git a/pcsd/bootstrap.rb b/pcsd/bootstrap.rb
d759a1
index ec6b535c..fc9d9b8c 100644
d759a1
--- a/pcsd/bootstrap.rb
d759a1
+++ b/pcsd/bootstrap.rb
d759a1
@@ -51,8 +51,23 @@ if not defined? $cur_node_name
d759a1
   $cur_node_name = `/bin/hostname`.chomp
d759a1
 end
d759a1
 
d759a1
-def configure_logger(log_device)
d759a1
-  logger = Logger.new(log_device)
d759a1
+def configure_logger()
d759a1
+  logger = Logger.new(StringIO.new())
d759a1
+  logger.formatter = proc {|severity, datetime, progname, msg|
d759a1
+    if Thread.current.key?(:pcsd_logger_container)
d759a1
+      Thread.current[:pcsd_logger_container] << {
d759a1
+        :level => severity,
d759a1
+        :timestamp_usec => (datetime.to_f * 1000000).to_i,
d759a1
+        :message => msg,
d759a1
+      }
d759a1
+    else
d759a1
+      STDERR.puts("#{datetime} #{progname} #{severity} #{msg}")
d759a1
+    end
d759a1
+  }
d759a1
+  return logger
d759a1
+end
d759a1
+
d759a1
+def early_log(logger)
d759a1
   if ENV['PCSD_DEBUG'] and ENV['PCSD_DEBUG'].downcase == "true" then
d759a1
     logger.level = Logger::DEBUG
d759a1
     logger.info "PCSD Debugging enabled"
d759a1
@@ -65,7 +80,6 @@ def configure_logger(log_device)
d759a1
   else
d759a1
     logger.debug "Detected systemd is not in use"
d759a1
   end
d759a1
-  return logger
d759a1
 end
d759a1
 
d759a1
 def get_capabilities(logger)
d759a1
diff --git a/pcsd/cfgsync.rb b/pcsd/cfgsync.rb
d759a1
index 16bcfbdc..1cab512e 100644
d759a1
--- a/pcsd/cfgsync.rb
d759a1
+++ b/pcsd/cfgsync.rb
d759a1
@@ -468,7 +468,8 @@ module Cfgsync
d759a1
       node_response = {}
d759a1
       threads = []
d759a1
       @nodes.each { |node|
d759a1
-        threads << Thread.new {
d759a1
+        threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+          Thread.current[:pcsd_logger_container] = logger
d759a1
           code, out = send_request_with_token(
d759a1
             @auth_user, node, 'set_configs', true, data, true, nil, 30,
d759a1
             @additional_known_hosts
d759a1
@@ -616,7 +617,8 @@ module Cfgsync
d759a1
       node_configs = {}
d759a1
       connected_to = {}
d759a1
       nodes.each { |node|
d759a1
-        threads << Thread.new {
d759a1
+        threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+          Thread.current[:pcsd_logger_container] = logger
d759a1
           code, out = send_request_with_token(
d759a1
             @auth_user, node, 'get_configs', false, data, true, nil, nil,
d759a1
             @additional_known_hosts
d759a1
diff --git a/pcsd/pcs.rb b/pcsd/pcs.rb
d759a1
index 7b991ac0..9a0efb46 100644
d759a1
--- a/pcsd/pcs.rb
d759a1
+++ b/pcsd/pcs.rb
d759a1
@@ -923,7 +923,8 @@ def is_auth_against_nodes(auth_user, node_names, timeout=10)
d759a1
   offline_nodes = []
d759a1
 
d759a1
   node_names.uniq.each { |node_name|
d759a1
-    threads << Thread.new {
d759a1
+    threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+      Thread.current[:pcsd_logger_container] = logger
d759a1
       code, response = send_request_with_token(
d759a1
         auth_user, node_name, 'check_auth', false, {}, true, nil, timeout
d759a1
       )
d759a1
@@ -963,7 +964,8 @@ def pcs_auth(auth_user, nodes)
d759a1
   auth_responses = {}
d759a1
   threads = []
d759a1
   nodes.each { |node_name, node_data|
d759a1
-    threads << Thread.new {
d759a1
+    threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+      Thread.current[:pcsd_logger_container] = logger
d759a1
       begin
d759a1
         addr = node_data.fetch('dest_list').fetch(0).fetch('addr')
d759a1
         port = node_data.fetch('dest_list').fetch(0).fetch('port')
d759a1
@@ -1199,7 +1201,8 @@ def cluster_status_from_nodes(auth_user, cluster_nodes, cluster_name)
d759a1
 
d759a1
   threads = []
d759a1
   cluster_nodes.uniq.each { |node|
d759a1
-    threads << Thread.new {
d759a1
+    threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+      Thread.current[:pcsd_logger_container] = logger
d759a1
       code, response = send_request_with_token(
d759a1
         auth_user,
d759a1
         node,
d759a1
diff --git a/pcsd/pcsd-cli.rb b/pcsd/pcsd-cli.rb
d759a1
index 942bae84..4daa93ba 100755
d759a1
--- a/pcsd/pcsd-cli.rb
d759a1
+++ b/pcsd/pcsd-cli.rb
d759a1
@@ -29,7 +29,8 @@ end
d759a1
 auth_user = {}
d759a1
 PCS = get_pcs_path()
d759a1
 $logger_device = StringIO.new
d759a1
-$logger = configure_logger($logger_device)
d759a1
+$logger = Logger.new($logger_device)
d759a1
+early_log($logger)
d759a1
 
d759a1
 capabilities, capabilities_pcsd = get_capabilities($logger)
d759a1
 CAPABILITIES = capabilities.freeze
d759a1
diff --git a/pcsd/pcsd-ruby.service b/pcsd/pcsd-ruby.service
d759a1
new file mode 100644
d759a1
index 00000000..deefdf4f
d759a1
--- /dev/null
d759a1
+++ b/pcsd/pcsd-ruby.service
d759a1
@@ -0,0 +1,20 @@
d759a1
+[Unit]
d759a1
+Description=PCS GUI and remote configuration interface (Ruby)
d759a1
+Documentation=man:pcsd(8)
d759a1
+Documentation=man:pcs(8)
d759a1
+Requires=network-online.target
d759a1
+After=network-online.target
d759a1
+# Stop the service automatically if nothing that depends on it is running
d759a1
+StopWhenUnneeded=true
d759a1
+# When stopping or restarting pcsd, stop or restart pcsd-ruby as well
d759a1
+PartOf=pcsd.service
d759a1
+
d759a1
+[Service]
d759a1
+EnvironmentFile=/etc/sysconfig/pcsd
d759a1
+Environment=GEM_HOME=/usr/lib/pcsd/vendor/bundle/ruby
d759a1
+# This file holds the selinux context
d759a1
+ExecStart=/usr/lib/pcsd/pcsd
d759a1
+Type=notify
d759a1
+
d759a1
+[Install]
d759a1
+WantedBy=multi-user.target
d759a1
diff --git a/pcsd/pcsd.conf b/pcsd/pcsd.conf
d759a1
index 4761c73f..a968f459 100644
d759a1
--- a/pcsd/pcsd.conf
d759a1
+++ b/pcsd/pcsd.conf
d759a1
@@ -38,3 +38,7 @@ PCSD_SESSION_LIFETIME=3600
d759a1
 #HTTPS_PROXY=
d759a1
 # Do not use proxy for specified hostnames
d759a1
 #NO_PROXY=
d759a1
+
d759a1
+
d759a1
+# Do not change
d759a1
+RACK_ENV=production
d759a1
diff --git a/pcsd/pcsd.rb b/pcsd/pcsd.rb
d759a1
index eff5c9a8..4cb98799 100644
d759a1
--- a/pcsd/pcsd.rb
d759a1
+++ b/pcsd/pcsd.rb
d759a1
@@ -22,6 +22,7 @@ require 'permissions.rb'
d759a1
 use Rack::CommonLogger
d759a1
 
d759a1
 set :app_file, __FILE__
d759a1
+set :logging, false
d759a1
 
d759a1
 def __msg_cluster_name_already_used(cluster_name)
d759a1
   return "The cluster name '#{cluster_name}' has already been added. You may not add two clusters with the same name."
d759a1
@@ -44,17 +45,17 @@ end
d759a1
 
d759a1
 def getAuthUser()
d759a1
   return {
d759a1
-    :username => $tornado_username,
d759a1
-    :usergroups => $tornado_groups,
d759a1
+    :username => Thread.current[:tornado_username],
d759a1
+    :usergroups => Thread.current[:tornado_groups],
d759a1
   }
d759a1
 end
d759a1
 
d759a1
 before do
d759a1
   # nobody is logged in yet
d759a1
   @auth_user = nil
d759a1
-  @tornado_session_username = $tornado_username
d759a1
-  @tornado_session_groups = $tornado_groups
d759a1
-  @tornado_is_authenticated = $tornado_is_authenticated
d759a1
+  @tornado_session_username = Thread.current[:tornado_username]
d759a1
+  @tornado_session_groups = Thread.current[:tornado_groups]
d759a1
+  @tornado_is_authenticated = Thread.current[:tornado_is_authenticated]
d759a1
 
d759a1
   if(request.path.start_with?('/remote/') and request.path != "/remote/auth") or request.path == '/run_pcs'
d759a1
     # Sets @auth_user to a hash containing info about logged in user or halts
d759a1
@@ -71,18 +72,8 @@ end
d759a1
 configure do
d759a1
   PCS = get_pcs_path()
d759a1
   PCS_INTERNAL = get_pcs_internal_path()
d759a1
-  $logger = configure_logger(StringIO.new())
d759a1
-  $logger.formatter = proc {|severity, datetime, progname, msg|
d759a1
-    # rushing a raw logging info into the global
d759a1
-    $tornado_logs << {
d759a1
-      :level => severity,
d759a1
-      :timestamp_usec => (datetime.to_f * 1000000).to_i,
d759a1
-      :message => msg,
d759a1
-    }
d759a1
-    # don't need any log to the stream
d759a1
-    ""
d759a1
-  }
d759a1
-
d759a1
+  $logger = configure_logger()
d759a1
+  early_log($logger)
d759a1
   capabilities, capabilities_pcsd = get_capabilities($logger)
d759a1
   CAPABILITIES = capabilities.freeze
d759a1
   CAPABILITIES_PCSD = capabilities_pcsd.freeze
d759a1
@@ -599,7 +590,8 @@ get '/manage/get_nodes_sw_versions' do
d759a1
     nodes = params[:node_list]
d759a1
   end
d759a1
   nodes.each {|node|
d759a1
-    threads << Thread.new {
d759a1
+    threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+      Thread.current[:pcsd_logger_container] = logger
d759a1
       code, response = send_request_with_token(
d759a1
         auth_user, node, 'get_sw_versions'
d759a1
       )
d759a1
@@ -625,7 +617,8 @@ post '/manage/auth_gui_against_nodes' do
d759a1
 
d759a1
   data = JSON.parse(params.fetch('data_json'))
d759a1
   data.fetch('nodes').each { |node_name, node_data|
d759a1
-    threads << Thread.new {
d759a1
+    threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+      Thread.current[:pcsd_logger_container] = logger
d759a1
       dest_list = node_data.fetch('dest_list')
d759a1
       addr = dest_list.fetch(0).fetch('addr')
d759a1
       port = dest_list.fetch(0).fetch('port')
d759a1
diff --git a/pcsd/pcsd.service b/pcsd/pcsd.service
d759a1
index 88d237af..0cab20ef 100644
d759a1
--- a/pcsd/pcsd.service
d759a1
+++ b/pcsd/pcsd.service
d759a1
@@ -4,6 +4,8 @@ Documentation=man:pcsd(8)
d759a1
 Documentation=man:pcs(8)
d759a1
 Requires=network-online.target
d759a1
 After=network-online.target
d759a1
+Requires=pcsd-ruby.service
d759a1
+After=pcsd-ruby.service
d759a1
 
d759a1
 [Service]
d759a1
 EnvironmentFile=/etc/sysconfig/pcsd
d759a1
diff --git a/pcsd/pcsd.service-runner b/pcsd/pcsd.service-runner
d759a1
new file mode 100644
d759a1
index 00000000..40c401fa
d759a1
--- /dev/null
d759a1
+++ b/pcsd/pcsd.service-runner
d759a1
@@ -0,0 +1,24 @@
d759a1
+#!/usr/bin/ruby
d759a1
+# This file is a runner for ruby part of pcsd callable from a systemd unit.
d759a1
+# It also serves as a holder of a selinux context.
d759a1
+
d759a1
+begin
d759a1
+  # add pcsd to the load path (ruby -I)
d759a1
+  libdir = File.dirname(__FILE__)
d759a1
+  $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
d759a1
+
d759a1
+  # change current directory (ruby -C)
d759a1
+  Dir.chdir('/var/lib/pcsd')
d759a1
+
d759a1
+  # import and run ruby daemon
d759a1
+  require 'rserver.rb'
d759a1
+rescue SignalException => e
d759a1
+  if [Signal.list['INT'], Signal.list['TERM']].include?(e.signo)
d759a1
+    # gracefully exit on SIGINT and SIGTERM
d759a1
+    # pcsd sets up signal handlers later, this catches exceptions which occur
d759a1
+    # by recieving signals before the handlers have been set up.
d759a1
+    exit
d759a1
+  else
d759a1
+    raise
d759a1
+  end
d759a1
+end
d759a1
diff --git a/pcsd/remote.rb b/pcsd/remote.rb
d759a1
index 28b91382..760d3374 100644
d759a1
--- a/pcsd/remote.rb
d759a1
+++ b/pcsd/remote.rb
d759a1
@@ -938,7 +938,8 @@ def status_all(params, request, auth_user, nodes=[], dont_update_config=false)
d759a1
   threads = []
d759a1
   forbidden_nodes = {}
d759a1
   nodes.each {|node|
d759a1
-    threads << Thread.new {
d759a1
+    threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+      Thread.current[:pcsd_logger_container] = logger
d759a1
       code, response = send_request_with_token(auth_user, node, 'status')
d759a1
       if 403 == code
d759a1
         forbidden_nodes[node] = true
d759a1
@@ -994,7 +995,8 @@ def clusters_overview(params, request, auth_user)
d759a1
   threads = []
d759a1
   config = PCSConfig.new(Cfgsync::PcsdSettings.from_file().text())
d759a1
   config.clusters.each { |cluster|
d759a1
-    threads << Thread.new {
d759a1
+    threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger|
d759a1
+      Thread.current[:pcsd_logger_container] = logger
d759a1
       cluster_map[cluster.name] = {
d759a1
         'cluster_name' => cluster.name,
d759a1
         'error_list' => [
d759a1
diff --git a/pcsd/rserver.rb b/pcsd/rserver.rb
d759a1
new file mode 100644
d759a1
index 00000000..6002a73c
d759a1
--- /dev/null
d759a1
+++ b/pcsd/rserver.rb
d759a1
@@ -0,0 +1,98 @@
d759a1
+require "base64"
d759a1
+require "date"
d759a1
+require "json"
d759a1
+require 'rack'
d759a1
+require 'sinatra'
d759a1
+require 'thin'
d759a1
+
d759a1
+require 'settings.rb'
d759a1
+
d759a1
+def pack_response(response)
d759a1
+  return [200, {}, [response.to_json.to_str]]
d759a1
+end
d759a1
+
d759a1
+def unpack_request(transport_env)
d759a1
+  return JSON.parse(Base64.strict_decode64(
d759a1
+    transport_env["rack.request.form_hash"]["TORNADO_REQUEST"]
d759a1
+  ))
d759a1
+end
d759a1
+
d759a1
+class TornadoCommunicationMiddleware
d759a1
+  def initialize(app)
d759a1
+    @app = app
d759a1
+  end
d759a1
+
d759a1
+  def call(transport_env)
d759a1
+    Thread.current[:pcsd_logger_container] = []
d759a1
+    begin
d759a1
+      request = unpack_request(transport_env)
d759a1
+
d759a1
+      if ["sinatra_gui", "sinatra_remote"].include?(request["type"])
d759a1
+        if request["type"] == "sinatra_gui"
d759a1
+          session = request["session"]
d759a1
+          Thread.current[:tornado_username] = session["username"]
d759a1
+          Thread.current[:tornado_groups] = session["groups"]
d759a1
+          Thread.current[:tornado_is_authenticated] = session["is_authenticated"]
d759a1
+        end
d759a1
+
d759a1
+        # Keys rack.input and rack.errors are required. We make sure they are
d759a1
+        # there.
d759a1
+        request_env = request["env"]
d759a1
+        request_env["rack.input"] = StringIO.new(request_env["rack.input"])
d759a1
+        request_env["rack.errors"] = StringIO.new()
d759a1
+
d759a1
+        status, headers, body = @app.call(request_env)
d759a1
+
d759a1
+        rack_errors = request_env['rack.errors'].string()
d759a1
+        if not rack_errors.empty?()
d759a1
+          $logger.error(rack_errors)
d759a1
+        end
d759a1
+
d759a1
+        return pack_response({
d759a1
+          :status => status,
d759a1
+          :headers => headers,
d759a1
+          :body => Base64.encode64(body.join("")),
d759a1
+          :logs => Thread.current[:pcsd_logger_container],
d759a1
+        })
d759a1
+      end
d759a1
+
d759a1
+      if request["type"] == "sync_configs"
d759a1
+        return pack_response({
d759a1
+          :next => Time.now.to_i + run_cfgsync(),
d759a1
+          :logs => Thread.current[:pcsd_logger_container],
d759a1
+        })
d759a1
+      end
d759a1
+
d759a1
+      raise "Unexpected value for key 'type': '#{request['type']}'"
d759a1
+    rescue => e
d759a1
+      return pack_response({:error => "Processing request error: '#{e}'"})
d759a1
+    end
d759a1
+  end
d759a1
+end
d759a1
+
d759a1
+
d759a1
+use TornadoCommunicationMiddleware
d759a1
+
d759a1
+require 'pcsd'
d759a1
+
d759a1
+::Rack::Handler.get('thin').run(Sinatra::Application, {
d759a1
+  :Host => PCSD_RUBY_SOCKET,
d759a1
+}) do |server|
d759a1
+  puts server.class
d759a1
+  server.threaded = true
d759a1
+  # notify systemd we are running
d759a1
+  if ISSYSTEMCTL
d759a1
+    if ENV['NOTIFY_SOCKET']
d759a1
+      socket_name = ENV['NOTIFY_SOCKET'].dup
d759a1
+      if socket_name.start_with?('@')
d759a1
+        # abstract namespace socket
d759a1
+        socket_name[0] = "\0"
d759a1
+      end
d759a1
+      $logger.info("Notifying systemd we are running (socket #{socket_name})")
d759a1
+      sd_socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
d759a1
+      sd_socket.connect(Socket.pack_sockaddr_un(socket_name))
d759a1
+      sd_socket.send('READY=1', 0)
d759a1
+      sd_socket.close()
d759a1
+    end
d759a1
+  end
d759a1
+end
d759a1
diff --git a/pcsd/settings.rb b/pcsd/settings.rb
d759a1
index e8dc0c96..4caa5b4c 100644
d759a1
--- a/pcsd/settings.rb
d759a1
+++ b/pcsd/settings.rb
d759a1
@@ -3,6 +3,7 @@ PCS_INTERNAL_EXEC = '/usr/lib/pcs/pcs_internal'
d759a1
 PCSD_EXEC_LOCATION = '/usr/lib/pcsd/'
d759a1
 PCSD_VAR_LOCATION = '/var/lib/pcsd/'
d759a1
 PCSD_DEFAULT_PORT = 2224
d759a1
+PCSD_RUBY_SOCKET = '/run/pcsd-ruby.socket'
d759a1
 
d759a1
 CRT_FILE = PCSD_VAR_LOCATION + 'pcsd.crt'
d759a1
 KEY_FILE = PCSD_VAR_LOCATION + 'pcsd.key'
d759a1
diff --git a/pcsd/settings.rb.debian b/pcsd/settings.rb.debian
d759a1
index daaae37b..c547bc51 100644
d759a1
--- a/pcsd/settings.rb.debian
d759a1
+++ b/pcsd/settings.rb.debian
d759a1
@@ -3,6 +3,7 @@ PCS_INTERNAL_EXEC = '/usr/lib/pcs/pcs_internal'
d759a1
 PCSD_EXEC_LOCATION = '/usr/share/pcsd/'
d759a1
 PCSD_VAR_LOCATION = '/var/lib/pcsd/'
d759a1
 PCSD_DEFAULT_PORT = 2224
d759a1
+PCSD_RUBY_SOCKET = '/run/pcsd-ruby.socket'
d759a1
 
d759a1
 CRT_FILE = PCSD_VAR_LOCATION + 'pcsd.crt'
d759a1
 KEY_FILE = PCSD_VAR_LOCATION + 'pcsd.key'
d759a1
diff --git a/pcsd/sinatra_cmdline_wrapper.rb b/pcsd/sinatra_cmdline_wrapper.rb
d759a1
deleted file mode 100644
d759a1
index f7b22008..00000000
d759a1
--- a/pcsd/sinatra_cmdline_wrapper.rb
d759a1
+++ /dev/null
d759a1
@@ -1,63 +0,0 @@
d759a1
-require "base64"
d759a1
-require "date"
d759a1
-require "json"
d759a1
-
d759a1
-request_json = ARGF.read()
d759a1
-
d759a1
-begin
d759a1
-  request = JSON.parse(request_json)
d759a1
-rescue => e
d759a1
-  puts e
d759a1
-  exit
d759a1
-end
d759a1
-
d759a1
-if !request.include?("type")
d759a1
-  result = {:error => "Type not specified"}
d759a1
-  print result.to_json
d759a1
-  exit
d759a1
-end
d759a1
-
d759a1
-$tornado_logs = []
d759a1
-
d759a1
-require 'pcsd'
d759a1
-
d759a1
-if ["sinatra_gui", "sinatra_remote"].include?(request["type"])
d759a1
-  if request["type"] == "sinatra_gui"
d759a1
-    $tornado_username = request["session"]["username"]
d759a1
-    $tornado_groups = request["session"]["groups"]
d759a1
-    $tornado_is_authenticated = request["session"]["is_authenticated"]
d759a1
-  end
d759a1
-
d759a1
-  set :logging, true
d759a1
-  set :run, false
d759a1
-  # Do not turn exceptions into fancy 100kB HTML pages and print them on stdout.
d759a1
-  # Instead, rack.errors is logged and therefore returned in result[:log].
d759a1
-  set :show_exceptions, false
d759a1
-  app = [Sinatra::Application][0]
d759a1
-
d759a1
-  env = request["env"]
d759a1
-  env["rack.input"] = StringIO.new(env["rack.input"])
d759a1
-  env["rack.errors"] = StringIO.new()
d759a1
-
d759a1
-  status, headers, body = app.call(env)
d759a1
-  rack_errors = env['rack.errors'].string()
d759a1
-  if not rack_errors.empty?()
d759a1
-    $logger.error(rack_errors)
d759a1
-  end
d759a1
-
d759a1
-  result = {
d759a1
-    :status => status,
d759a1
-    :headers => headers,
d759a1
-    :body => Base64.encode64(body.join("")),
d759a1
-  }
d759a1
-
d759a1
-elsif request["type"] == "sync_configs"
d759a1
-  result = {
d759a1
-    :next => Time.now.to_i + run_cfgsync()
d759a1
-  }
d759a1
-else
d759a1
-  result = {:error => "Unknown type: '#{request["type"]}'"}
d759a1
-end
d759a1
-
d759a1
-result[:logs] = $tornado_logs
d759a1
-print result.to_json
d759a1
-- 
d759a1
2.21.1
d759a1