diff --git a/patroni/__init__.py b/patroni/__init__.py index ee4c5824..18df8186 100644 --- a/patroni/__init__.py +++ b/patroni/__init__.py @@ -125,7 +125,8 @@ class Patroni(object): def setup_signal_handlers(self): self._received_sighup = False self._received_sigterm = False - signal.signal(signal.SIGHUP, self.sighup_handler) + if os.name != 'nt': + signal.signal(signal.SIGHUP, self.sighup_handler) signal.signal(signal.SIGTERM, self.sigterm_handler) def shutdown(self): @@ -154,6 +155,8 @@ def patroni_main(): def pg_ctl_start(args): import subprocess + if os.name != 'nt': + os.setsid() postmaster = subprocess.Popen(args) print(postmaster.pid) @@ -197,11 +200,13 @@ def main(): os.kill(pid, signo) signal.signal(signal.SIGCHLD, sigchld_handler) - signal.signal(signal.SIGHUP, passtochild) + if os.name != 'nt': + signal.signal(signal.SIGHUP, passtochild) + signal.signal(signal.SIGQUIT, passtochild) signal.signal(signal.SIGINT, passtochild) signal.signal(signal.SIGUSR1, passtochild) signal.signal(signal.SIGUSR2, passtochild) - signal.signal(signal.SIGQUIT, passtochild) + signal.signal(signal.SIGABRT, passtochild) signal.signal(signal.SIGTERM, passtochild) patroni = call_self(sys.argv[1:]) diff --git a/patroni/api.py b/patroni/api.py index d2f52d03..d254d481 100644 --- a/patroni/api.py +++ b/patroni/api.py @@ -1,11 +1,11 @@ import base64 -import fcntl import json import logging import psycopg2 import time import dateutil.parser import datetime +import os from patroni.postgresql import PostgresConnectionException, PostgresException, Postgresql from patroni.utils import deep_compare, parse_bool, patch_config, Retry, \ @@ -479,8 +479,10 @@ class RestApiServer(ThreadingMixIn, HTTPServer, Thread): @staticmethod def _set_fd_cloexec(fd): - flags = fcntl.fcntl(fd, fcntl.F_GETFD) - fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + if os.name != 'nt': + import fcntl + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) def check_basic_auth_key(self, key): return self.__auth_key == key diff --git a/patroni/config.py b/patroni/config.py index dc8a6045..d5a91529 100644 --- a/patroni/config.py +++ b/patroni/config.py @@ -128,7 +128,7 @@ class Config(object): with os.fdopen(fd, 'w') as f: fd = None json.dump(self.dynamic_configuration, f) - tmpfile = os.rename(tmpfile, self._cache_file) + tmpfile = os.replace(tmpfile, self._cache_file) self._cache_needs_saving = False except Exception: logger.exception('Exception when saving file: %s', self._cache_file) diff --git a/patroni/dcs/__init__.py b/patroni/dcs/__init__.py index 8d050fd5..be30daea 100644 --- a/patroni/dcs/__init__.py +++ b/patroni/dcs/__init__.py @@ -419,8 +419,9 @@ class AbstractDCS(object): :param config: dict, reference to config section of selected DCS. i.e.: `zookeeper` for zookeeper, `etcd` for etcd, etc... """ + import re self._name = config['name'] - self._base_path = os.path.join('/', config.get('namespace', '/service/').strip('/'), config['scope']) + self._base_path = re.sub('/+', '/', '/'.join(['', config.get('namespace', 'service'), config['scope']])) self._set_loop_wait(config.get('loop_wait', 10)) self._ctl = bool(config.get('patronictl', False)) diff --git a/patroni/dcs/consul.py b/patroni/dcs/consul.py index f65eeb2a..ef7f494f 100644 --- a/patroni/dcs/consul.py +++ b/patroni/dcs/consul.py @@ -288,7 +288,7 @@ class Consul(AbstractDCS): nodes = {} for node in results: node['Value'] = (node['Value'] or b'').decode('utf-8') - nodes[os.path.relpath(node['Key'], path)] = node + nodes[os.path.relpath(node['Key'], path).replace('\\', '/')] = node # get initialize flag initialize = nodes.get(self._INITIALIZE) diff --git a/patroni/dcs/etcd.py b/patroni/dcs/etcd.py index 5d664a72..b03f6d39 100644 --- a/patroni/dcs/etcd.py +++ b/patroni/dcs/etcd.py @@ -448,7 +448,7 @@ class Etcd(AbstractDCS): def _load_cluster(self): try: result = self.retry(self._client.read, self.client_path(''), recursive=True) - nodes = {os.path.relpath(node.key, result.key): node for node in result.leaves} + nodes = {os.path.relpath(node.key, result.key).replace('\\', '/'): node for node in result.leaves} # get initialize flag initialize = nodes.get(self._INITIALIZE) diff --git a/patroni/postgresql.py b/patroni/postgresql.py index f69a52bb..306dbb57 100644 --- a/patroni/postgresql.py +++ b/patroni/postgresql.py @@ -640,7 +640,8 @@ class Postgresql(object): return os.environ.copy() with open(self._pgpass, 'w') as f: - os.fchmod(f.fileno(), 0o600) + if os.name != 'nt': + os.fchmod(f.fileno(), 0o600) f.write('{host}:{port}:*:{user}:{password}\n'.format(**record)) env = os.environ.copy() @@ -1124,9 +1125,10 @@ class Postgresql(object): # therefore we need to make sure that hba_file is not overriden # after changing superuser password we will "revert" all these "changes" if self._running_custom_bootstrap or 'hba_file' not in self._server_parameters: - f.write("hba_file = '{0}'\n".format(self._pg_hba_conf)) + f.write("hba_file = '{0}'\n".format(self._pg_hba_conf.replace('\\', '\\\\'))) if 'ident_file' not in self._server_parameters: - f.write("ident_file = '{0}'\n".format(os.path.join(self._config_dir, 'pg_ident.conf'))) + s = "ident_file = '{0}'\n".format(os.path.join(self._config_dir, 'pg_ident.conf').replace('\\', '\\\\')) + f.write(s) def is_healthy(self): if not self.is_running(): @@ -1221,8 +1223,10 @@ class Postgresql(object): # Don't try to call pg_controldata during backup restore if self._version_file_exists() and self.state != 'creating replica': try: - data = subprocess.check_output([self._pgcommand('pg_controldata'), self._data_dir], - env={'LANG': 'C', 'LC_ALL': 'C', 'PATH': os.environ['PATH']}) + env = {'LANG': 'C', 'LC_ALL': 'C', 'PATH': os.getenv('PATH')} + if os.getenv('SYSTEMROOT') is not None: + env['SYSTEMROOT'] = os.getenv('SYSTEMROOT') + data = subprocess.check_output([self._pgcommand('pg_controldata'), self._data_dir], env=env) if data: data = data.decode('utf-8').splitlines() # pg_controldata output depends on major verion. Some of parameters are prefixed by 'Current ' diff --git a/patroni/postmaster.py b/patroni/postmaster.py index d07a62f1..a57f2b87 100644 --- a/patroni/postmaster.py +++ b/patroni/postmaster.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) STOP_SIGNALS = { 'smart': signal.SIGTERM, 'fast': signal.SIGINT, - 'immediate': signal.SIGQUIT, + 'immediate': signal.SIGQUIT if os.name != 'nt' else signal.SIGABRT, } @@ -138,7 +138,8 @@ class PostmasterProcess(psutil.Process): # of init process to take care about postmaster. # In order to make everything portable we can't use fork&exec approach here, so we will call # ourselves and pass list of arguments which must be used to start postgres. - env = {p: os.environ[p] for p in ('PATH', 'LD_LIBRARY_PATH', 'LC_ALL', 'LANG') if p in os.environ} + # On Windows, in order to run a side-by-side assembly the specified env must include a valid SYSTEMROOT. + env = {p: os.environ[p] for p in ('PATH', 'LD_LIBRARY_PATH', 'LC_ALL', 'LANG', 'SYSTEMROOT') if p in os.environ} try: proc = PostmasterProcess._from_pidfile(data_dir) if proc and not proc._is_postmaster_process(): @@ -153,10 +154,11 @@ class PostmasterProcess(psutil.Process): env['PG_GRANDPARENT_PID'] = str(proc.pid) except psutil.NoSuchProcess: pass - - proc = call_self(['pg_ctl_start', pgcommand, '-D', data_dir, - '--config-file={}'.format(conf)] + options, close_fds=True, - preexec_fn=os.setsid, stdout=subprocess.PIPE, env=env) + cmdline = [pgcommand, '-D', data_dir, '--config-file={}'.format(conf)] + options + logger.debug("Starting postgres: %s", " ".join(cmdline)) + proc = call_self(['pg_ctl_start'] + cmdline, + close_fds=(os.name != 'nt'), stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, env=env) pid = int(proc.stdout.readline().strip()) proc.wait() logger.info('postmaster pid=%s', pid) diff --git a/setup.py b/setup.py index d81011ef..53f86e50 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ CLASSIFIERS = [ 'Operating System :: MacOS', 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: BSD :: FreeBSD', + 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3',