File: applicationinstance.py

package info (click to toggle)
backintime 1.5.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,632 kB
  • sloc: python: 23,454; sh: 859; makefile: 172; xml: 62
file content (259 lines) | stat: -rw-r--r-- 8,759 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# SPDX-FileCopyrightText: © 2008-2022 Oprea Dan
# SPDX-FileCopyrightText: © 2008-2022 Bart de Koning
# SPDX-FileCopyrightText: © 2008-2022 Richard Bailey
# SPDX-FileCopyrightText: © 2008-2022 Germar Reitze
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
# This file is part of the program "Back In Time" which is released under GNU
# General Public License v2 (GPLv2). See LICENSES directory or go to
# <https://spdx.org/licenses/GPL-2.0-or-later.html>.
"""
This module holds the ApplicationInstance class, used to handle
the one application instance mechanism.
"""
import os
import fcntl
import logger
import tools


# TODO This class name is a misnomer (there may be more than
#      one app instance eg. if a restore is running and another
#      backup starts).
#      Rename it to eg. LockFileManager
# TODO2 When refactoring have a look at "common/flock.py" still implementing
#       a contxt manager for that problem.
class ApplicationInstance:
    """
    Class used to handle one application instance mechanism.

    Args:
        pidFile (str):      full path of file used to save pid and procname
        autoExit (bool):    automatically call sys.exit if there is an other
                            instance running
        flock (bool):       use file-locks to make sure only one instance
                            is checking at the same time
    """

    def __init__(self, pidFile, autoExit=True, flock=False):
        self.pidFile = pidFile
        self.pid = 0
        self.procname = ''
        self.flock = None

        if flock:
            self.flockExclusiv()

        if autoExit:
            if self.check(True):
                self.startApplication()

    def __del__(self):
        self.flockUnlock()

    # TODO Rename to is_single_instance() to make the purpose more obvious
    def check(self, autoExit=False):
        """Check if the current application is already running.

        Args:
            autoExit (bool): Automatically call ``sys.exit()`` if there is
                another instance running.

        Returns:
            bool: ``True`` if this is the only application instance.
        """
        # check if the pidfile exists
        if not os.path.isfile(self.pidFile):
            return True

        self.pid, self.procname = self.readPidFile()

        # check if the process (PID) that created the pid file still exists
        if 0 == self.pid:
            return True

        if not tools.processAlive(self.pid):
            return True

        # check if the process has the same procname
        # check cmdline for backwards compatibility
        if self.procname and                                    \
           self.procname != tools.processName(self.pid) and    \
           self.procname != tools.processCmdline(self.pid):
            return True

        if autoExit:
            # exit the application
            print("The application is already running !")

            # exit raise an exception so don't put it in a try/except block
            exit(0)

        return False

    def busy(self):
        """
        Check if one application with this instance is currently running.

        Returns:
            bool:       ``True`` if an other instance is currently running.
        """
        return not self.check()

    def startApplication(self):
        """
        Called when the single instance starts to save its pid
        """
        pid = os.getpid()
        procname = tools.processName(pid)

        try:
            with open(self.pidFile, 'wt') as f:
                f.write('{}\n{}'.format(pid, procname))

        except OSError as e:
            logger.error(
                'Failed to write PID file %s: [%s] %s'
                % (e.filename, e.errno, e.strerror))

        # The flock is removed here because it shall only ensure serialized access to the "pidFile" (lock file):
        # Without setting flock in __init__ another process could also check for the existences of the "pidFile"
        # in parallel and also create a new one (overwriting the one created here).
        self.flockUnlock()

    def exitApplication(self):
        """
        Called when the single instance exit (remove pid file)
        """
        try:
            os.remove(self.pidFile)
        except:
            pass

    def flockExclusiv(self):
        """
        Create an exclusive advisory file lock named <PID file>.flock
        to block the creation of a second instance while the first instance
        is still in the process of starting (but has not yet completely
        started).

        The purpose is to make
        1. the check if the PID lock file already exists
        2. and the subsequent creation of the PID lock file
        an atomic operation by using a blocking "flock" file lock
        on a second file to avoid that two or more processes check for
        an existing PID lock file, find none and create a new one
        (so that only the last creator wins).

        Dev notes:
        ----------
        buhtz (2023-09):
        Not sure but just log an ERROR without doing anything else is
        IMHO not enough.

        aryoda (2023-12):
        It seems the purpose of this additional lock file using an exclusive lock
        is to block the other process to continue until this exclusive lock
        is released (= serialize execution).
        Therefore advisory locks are used via fcntl.flock (see: man 2 fcntl)

        buhtz (2024-05):
        Have a look at the new :mod:`flock` module providing an flock context
        manager.
        """

        flock_file_URI = self.pidFile + '.flock'
        logger.debug(f"Trying to put an advisory lock on the flock file {flock_file_URI}")

        try:
            self.flock = open(flock_file_URI, 'w')
            # This opens an advisory lock which which is considered only
            # if other processes cooperate by explicitly acquiring locks
            # (which BiT does IMHO).
            # TODO Does this lock request really block if another processes
            #      already holds a lock (until the lock is released)?
            #      = Is the execution serialized?
            # Provisional answer:
            #      Yes, fcntl.flock seems to wait until the lock is removed
            #      from the file.
            #      Tested by starting one process in the console via
            #         python3 applicationinstance.py
            #      and then (while the first process is still running)
            #      the same command in a 2nd terminal.
            #      The ApplicationInstance constructor call needs
            #      to be changed for this by adding "False, True"
            #      to trigger an exclusive lock.
            fcntl.flock(self.flock, fcntl.LOCK_EX)

        except OSError as e:
            logger.error('Failed to write flock file %s: [%s] %s'
                         % (e.filename, e.errno, e.strerror))

    def flockUnlock(self):
        """
        Remove the exclusive lock. Second instance can now continue
        but should find it self to be obsolete.
        """
        if self.flock:
            logger.debug(f"Trying to remove the advisory lock from the flock file {self.flock.name}")
            fcntl.fcntl(self.flock, fcntl.LOCK_UN)
            self.flock.close()

            try:
                os.remove(self.flock.name)
            except:
                # an other instance was faster
                # race condition while using 'if os.path.exists(...)'
                pass

        self.flock = None

    def readPidFile(self):
        """
        Read the pid and procname from the file

        Returns:
            tuple:  tuple of (pid(int), procname(str))
        """
        pid = 0
        procname = ''

        try:
            with open(self.pidFile, 'rt') as f:
                data = f.read()

            data = data.split('\n', 1)

            if data[0].isdigit():
                pid = int(data[0])

            if len(data) > 1:
                procname = data[1].strip('\n')

        except OSError as e:
            logger.warning(
                'Failed to read PID and process name from %s: [%s] %s'
                % (e.filename, e.errno, e.strerror))

        except ValueError as e:
            logger.warning(
                'Failed to extract PID and process name from %s: %s'
                % (self.pidFile, str(e)))

        return (pid, procname)


if __name__ == '__main__':
    import time

    # create application instance
    appInstance = ApplicationInstance('/tmp/myapp.pid')

    # do something here
    print("Start MyApp")
    time.sleep(5)  # sleep 5 seconds
    print("End MyApp")

    # remove pid file
    appInstance.exitApplication()