Blob Blame History Raw
From d54c102cee7a61dd3eccd62d60af218aa97a85fc Mon Sep 17 00:00:00 2001
From: Ivan Devat <idevat@redhat.com>
Date: Thu, 9 Jan 2020 15:53:37 +0100
Subject: [PATCH 6/7] squash bz1783106 fix-sinatra-wrapper-performance-issue

create prototype of tornado - thin communication

put socket path to settings

don't mix logs from threads in ruby daemon

run ruby daemon via systemd units

support trailing slash by gui urls e.g. /manage/

decode body from ruby response for log

configure ruby wrapper by socket path

remove env values not used for ruby calls any more

deal with ruby daemon communication issues

fix tests

cleanup ruby server code

deal with errors from ruby daemon in python daemon

remove unused cmdline wrapper

add ruby daemon infrastructure to spec etc.

stop logging to stderr from ruby daemon

fix spec file

* add missing cp for new rubygems
* make sure to start the new ruby daemon on package upgrade
* tests: give the new daemon enough time to start
---
 .gitlab-ci.yml                            |   7 +-
 Makefile                                  |   6 +
 pcs.spec.in                               |  30 ++++-
 pcs/daemon/app/sinatra_ui.py              |   2 +-
 pcs/daemon/env.py                         |  36 ------
 pcs/daemon/ruby_pcsd.py                   | 136 +++++++++++-----------
 pcs/daemon/run.py                         |   8 +-
 pcs/settings_default.py                   |   1 +
 pcs_test/tier0/daemon/app/fixtures_app.py |   3 +-
 pcs_test/tier0/daemon/test_env.py         |  66 +----------
 pcs_test/tier0/daemon/test_ruby_pcsd.py   |  13 +--
 pcsd/Gemfile                              |   1 +
 pcsd/Gemfile.lock                         |   7 ++
 pcsd/Makefile                             |   3 +
 pcsd/bootstrap.rb                         |  20 +++-
 pcsd/cfgsync.rb                           |   6 +-
 pcsd/pcs.rb                               |   9 +-
 pcsd/pcsd-cli.rb                          |   3 +-
 pcsd/pcsd-ruby.service                    |  20 ++++
 pcsd/pcsd.conf                            |   4 +
 pcsd/pcsd.rb                              |  31 ++---
 pcsd/pcsd.service                         |   2 +
 pcsd/pcsd.service-runner                  |  24 ++++
 pcsd/remote.rb                            |   6 +-
 pcsd/rserver.rb                           |  98 ++++++++++++++++
 pcsd/settings.rb                          |   1 +
 pcsd/settings.rb.debian                   |   1 +
 pcsd/sinatra_cmdline_wrapper.rb           |  63 ----------
 28 files changed, 330 insertions(+), 277 deletions(-)
 create mode 100644 pcsd/pcsd-ruby.service
 create mode 100644 pcsd/pcsd.service-runner
 create mode 100644 pcsd/rserver.rb
 delete mode 100644 pcsd/sinatra_cmdline_wrapper.rb

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