|
17 | 17 | # along with pytest-postgresql. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 | """PostgreSQL executor crafter around pg_ctl.""" |
19 | 19 |
|
| 20 | +import os |
20 | 21 | import os.path |
21 | 22 | import platform |
22 | 23 | import re |
23 | 24 | import shutil |
| 25 | +import signal |
24 | 26 | import subprocess |
25 | 27 | import tempfile |
26 | 28 | import time |
@@ -48,13 +50,20 @@ class PostgreSQLExecutor(TCPExecutor): |
48 | 50 | <http://www.postgresql.org/docs/current/static/app-pg-ctl.html>`_ |
49 | 51 | """ |
50 | 52 |
|
51 | | - BASE_PROC_START_COMMAND = ( |
52 | | - '{executable} start -D "{datadir}" ' |
53 | | - "-o \"-F -p {port} -c log_destination='stderr' " |
54 | | - "-c logging_collector=off " |
55 | | - "-c unix_socket_directories='{unixsocketdir}' {postgres_options}\" " |
56 | | - '-l "{logfile}" {startparams}' |
57 | | - ) |
| 53 | + def _get_base_command(self) -> str: |
| 54 | + """Get the base PostgreSQL command, cross-platform compatible.""" |
| 55 | + # Use unified format without single quotes around values |
| 56 | + # This format works on both Windows and Unix systems since PostgreSQL |
| 57 | + # configuration values without spaces don't require quotes |
| 58 | + return ( |
| 59 | + '{executable} start -D "{datadir}" ' |
| 60 | + '-o "-F -p {port} -c log_destination=stderr ' |
| 61 | + "-c logging_collector=off " |
| 62 | + '-c unix_socket_directories={unixsocketdir} {postgres_options}" ' |
| 63 | + '-l "{logfile}" {startparams}' |
| 64 | + ) |
| 65 | + |
| 66 | + BASE_PROC_START_COMMAND = "" # Will be set dynamically |
58 | 67 |
|
59 | 68 | VERSION_RE = re.compile(r".* (?P<version>\d+(?:\.\d+)?)") |
60 | 69 | MIN_SUPPORTED_VERSION = parse("14") |
@@ -108,7 +117,7 @@ def __init__( |
108 | 117 | self.logfile = logfile |
109 | 118 | self.startparams = startparams |
110 | 119 | self.postgres_options = postgres_options |
111 | | - command = self.BASE_PROC_START_COMMAND.format( |
| 120 | + command = self._get_base_command().format( |
112 | 121 | executable=self.executable, |
113 | 122 | datadir=self.datadir, |
114 | 123 | port=port, |
@@ -219,17 +228,58 @@ def running(self) -> bool: |
219 | 228 | status_code = subprocess.getstatusoutput(f'{self.executable} status -D "{self.datadir}"')[0] |
220 | 229 | return status_code == 0 |
221 | 230 |
|
| 231 | + def _windows_terminate_process(self, sig: Optional[int] = None) -> None: |
| 232 | + """Terminate process on Windows.""" |
| 233 | + if self.process is None: |
| 234 | + return |
| 235 | + |
| 236 | + try: |
| 237 | + # On Windows, try to terminate gracefully first |
| 238 | + self.process.terminate() |
| 239 | + # Give it a chance to terminate gracefully |
| 240 | + try: |
| 241 | + self.process.wait(timeout=5) |
| 242 | + except subprocess.TimeoutExpired: |
| 243 | + # If it doesn't terminate gracefully, force kill |
| 244 | + self.process.kill() |
| 245 | + self.process.wait() |
| 246 | + except (OSError, AttributeError): |
| 247 | + # Process might already be dead or other issues |
| 248 | + pass |
| 249 | + |
| 250 | + def _unix_terminate_process(self, sig: Optional[int] = None) -> None: |
| 251 | + """Terminate process on Unix systems.""" |
| 252 | + if self.process is None: |
| 253 | + return |
| 254 | + |
| 255 | + try: |
| 256 | + # On Unix systems, use the signal |
| 257 | + actual_sig = sig or signal.SIGTERM |
| 258 | + os.killpg(self.process.pid, actual_sig) |
| 259 | + except (OSError, AttributeError): |
| 260 | + # Process might already be dead or other issues |
| 261 | + pass |
| 262 | + |
222 | 263 | def stop(self: T, sig: Optional[int] = None, exp_sig: Optional[int] = None) -> T: |
223 | 264 | """Issue a stop request to executable.""" |
224 | 265 | subprocess.check_output( |
225 | 266 | f'{self.executable} stop -D "{self.datadir}" -m f', |
226 | 267 | shell=True, |
227 | 268 | ) |
228 | 269 | try: |
229 | | - super().stop(sig, exp_sig) |
| 270 | + if platform.system() == "Windows": |
| 271 | + self._windows_terminate_process(sig) |
| 272 | + else: |
| 273 | + super().stop(sig, exp_sig) |
230 | 274 | except ProcessFinishedWithError: |
231 | 275 | # Finished, leftovers ought to be cleaned afterwards anyway |
232 | 276 | pass |
| 277 | + except AttributeError as e: |
| 278 | + # Handle case where os.killpg doesn't exist (shouldn't happen now) |
| 279 | + if "killpg" in str(e): |
| 280 | + self._windows_terminate_process(sig) |
| 281 | + else: |
| 282 | + raise |
233 | 283 | return self |
234 | 284 |
|
235 | 285 | def __del__(self) -> None: |
|
0 commit comments