Source code for glideinwms.lib.pidSupport
# SPDX-FileCopyrightText: 2009 Fermi Research Alliance, LLC
# SPDX-License-Identifier: Apache-2.0
# Description:
# Handle pid lock files
import fcntl
import os
import os.path
import signal
import time
############################################################
#
# Verify if the system knows about a pid
#
[docs]
def check_pid(pid):
return os.path.isfile(f"/proc/{pid}/cmdline")
############################################################
# this exception is raised when trying to register a pid
# but another process is already owning the PID file
[docs]
class AlreadyRunning(RuntimeError):
pass
#######################################################
#
# self.mypid is valid only if self.fd is valid
# or after a load
[docs]
class PidSupport:
def __init__(self, pid_fname):
self.pid_fname = pid_fname
self.fd = None
self.mypid = None
self.lock_in_place = False
# open the pid_file and gain the exclusive lock
# also write in the PID information
[docs]
def register(self, pid=None, started_time=None): # if none, will default to os.getpid() # if none, use time.time()
if self.fd is not None:
raise RuntimeError("Cannot register two pids in the same object!")
if pid is None:
pid = os.getpid()
if started_time is None:
started_time = time.time()
self.mypid = pid
self.started_time = started_time
# check lock file
if not os.path.exists(self.pid_fname):
# create a lock file if needed
fd = open(self.pid_fname, "w")
fd.close()
# Do not use 'with' or close the file. Will be closed when lock is released
fd = open(self.pid_fname, "r+")
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
self.lock_in_place = True
except OSError as e:
fd.close()
raise AlreadyRunning(f"Another process already running. Unable to acquire lock {self.pid_fname}") from e
fd.seek(0)
fd.truncate()
fd.write(self.format_pid_file_content())
fd.flush()
self.fd = fd
return
# release the lock on the PID file
# also purge the info from the file
[docs]
def relinquish(self):
self.fd.seek(0)
self.fd.truncate()
self.fd.flush()
self.fd.close()
self.fd = None
self.mypid = None
self.lock_in_place = False
# Will update self.mypid and self.lock_in_place
[docs]
def load_registered(self):
if self.fd is not None:
return # we own it, so nothing to do
# make sure it is initialized (to not registered)
self.reset_to_default()
self.lock_in_place = False
# else I don't own it
if not os.path.isfile(self.pid_fname):
return
with open(self.pid_fname) as fd:
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
# if I can get a lock, it means that there is no process
return
except OSError:
# there is a process
# I will read it even if locked, so that I can report what the PID is
# if the data is corrupted, I will deal with it later
lines = fd.readlines()
self.lock_in_place = True
try:
self.parse_pid_file_content(lines)
except Exception:
# Data is corrupted, cannot get the PID, masking exceptions
return
if not check_pid(self.mypid):
# found not running
self.mypid = None
return
###############################
# INTERNAL
# Can be redefined by children
###############################
[docs]
def format_pid_file_content(self):
return f"PID: {self.mypid}\nStarted: {time.ctime(self.started_time)}\n"
[docs]
def reset_to_default(self):
self.mypid = None
[docs]
def parse_pid_file_content(self, lines):
self.mypid = None
if len(lines) < 2:
raise RuntimeError("Corrupted lock file: too short")
pidarr = lines[0].split()
if (len(pidarr) != 2) or (pidarr[0] != "PID:"):
raise RuntimeError("Corrupted lock file: no PID")
try:
pid = int(pidarr[1])
except Exception:
raise RuntimeError("Corrupted lock file: invalid PID") from None
self.mypid = pid
return
#######################################################
#
# self.mypid and self.parent_pid are valid only
# if self.fd is valid or after a load
[docs]
class PidWParentSupport(PidSupport):
def __init__(self, pid_fname):
PidSupport.__init__(self, pid_fname)
self.parent_pid = None
# open the pid_file and gain the exclusive lock
# also write in the PID information
[docs]
def register(
self, parent_pid, pid=None, started_time=None # if none, will default to os.getpid()
): # if none, use time.time()
if self.fd is not None:
raise RuntimeError("Cannot register two pids in the same object!")
self.parent_pid = parent_pid
PidSupport.register(self, pid, started_time)
###############################
# INTERNAL
# Can be redefined by children
###############################
[docs]
def format_pid_file_content(self):
return f"PID: {self.mypid}\nParent PID:{self.parent_pid}\nStarted: {time.ctime(self.started_time)}\n"
[docs]
def reset_to_default(self):
PidSupport.reset_to_default(self)
self.parent_pid = None
[docs]
def parse_pid_file_content(self, lines):
self.mypid = None
self.parent_pid = None
if len(lines) < 3:
raise RuntimeError("Corrupted lock file: too short")
pidarr = lines[0].split()
if (len(pidarr) != 2) or (pidarr[0] != "PID:"):
raise RuntimeError("Corrupted lock file: no PID")
try:
pid = int(pidarr[1])
except Exception:
raise RuntimeError("Corrupted lock file: invalid PID") from None
pidarr = lines[1].split(":")
if (len(pidarr) != 2) or (pidarr[0] != "Parent PID"):
raise RuntimeError("Corrupted lock file: no Parent PID")
try:
parent_pid = int(pidarr[1])
except Exception:
raise RuntimeError("Corrupted lock file: invalid Parent PID") from None
self.mypid = pid
self.parent_pid = parent_pid
return
[docs]
def termsignal(signr, frame):
raise KeyboardInterrupt("Received signal %s" % signr)
[docs]
def register_sighandler():
signal.signal(signal.SIGTERM, termsignal)
signal.signal(signal.SIGQUIT, termsignal)
[docs]
def unregister_sighandler():
signal.signal(signal.SIGTERM, signal.SIG_DFL)
signal.signal(signal.SIGQUIT, signal.SIG_DFL)