
# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/

import os
import errno

from multiprocessing.util import Finalize


class LockHeld(Exception):
    pass


class DaemonLock(object):
    """daemon locking
    USAGE:
    try:
        l = DaemonLock(file_='/path/tolockfile',desc='test lock')
        main()
        l.release()
    except LockHeld:
        sys.exit(1)
    """

    def __init__(self, file_=None, callbackfn=None,
                 desc='daemon lock', debug=False):

        lock_name = os.path.join(os.path.dirname(__file__), 'running.lock')
        self.pidfile = file_ if file_ else lock_name
        self.callbackfn = callbackfn
        self.desc = desc
        self.debug = debug
        self.held = False
        # run the lock automatically !
        self.lock()
        self._finalize = Finalize(self, DaemonLock._on_finalize,
                                  args=(self, debug), exitpriority=10)

    @staticmethod
    def _on_finalize(lock, debug):
        if lock.held:
            if debug:
                print('lock held finalazing and running lock.release()')
            lock.release()

    def lock(self):
        """
        locking function, if lock is present it
        will raise LockHeld exception
        """
        lockname = f'{os.getpid()}'
        if self.debug:
            print('running lock')
        self.trylock()
        self.makelock(lockname, self.pidfile)
        return True

    def trylock(self):
        running_pid = False
        if self.debug:
            print('checking for already running process')
        try:
            with open(self.pidfile, 'r') as f:
                try:
                    running_pid = int(f.readline())
                except ValueError:
                    running_pid = -1

            if self.debug:
                print(f'lock file present running_pid: {running_pid}, '
                      f'checking for execution')
            # Now we check the PID from lock file matches to the current
            # process PID
            if running_pid:
                try:
                    os.kill(running_pid, 0)
                except OSError as exc:
                    if exc.errno in (errno.ESRCH, errno.EPERM):
                        print("Lock File is there but the program is not running")
                        print(f"Removing lock file for the: {running_pid}")
                        self.release()
                    else:
                        raise
                else:
                    print("You already have an instance of the program running")
                    print(f"It is running as process {running_pid}")
                    raise LockHeld()

        except IOError as e:
            if e.errno != 2:
                raise

    def release(self):
        """releases the pid by removing the pidfile
        """
        if self.debug:
            print('trying to release the pidlock')

        if self.callbackfn:
            # execute callback function on release
            if self.debug:
                print(f'executing callback function {self.callbackfn}')
            self.callbackfn()
        try:
            if self.debug:
                print(f'removing pidfile {self.pidfile}')
            os.remove(self.pidfile)
            self.held = False
        except OSError as e:
            if self.debug:
                print(f'removing pidfile failed {e}')
            pass

    def makelock(self, lockname, pidfile):
        """
        this function will make an actual lock

        :param lockname: acctual pid of file
        :param pidfile: the file to write the pid in
        """
        if self.debug:
            print(f'creating a file {lockname} and pid: {pidfile}')

        dir_, file_ = os.path.split(pidfile)
        if not os.path.isdir(dir_):
            os.makedirs(dir_)
        with open(self.pidfile, 'wb') as f:
            f.write(lockname)
        self.held = True
