# Session timeout tool for cherrypy
# Copyright (C) 2012-2026 IKUS Software
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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 General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# BFH Science Self-Service Portal
# Copyright (C) 2025-2026 IKUS Software
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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 General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
import time
from contextlib import contextmanager

import cherrypy
import zc.lockfile
from cherrypy.lib import locking
from cherrypy.lib.sessions import FileSession as CPFileSession


# Issue in cherrypy: https://github.com/cherrypy/cherrypy/issues/2065
class FileSession(CPFileSession):
    """
    Override implementation of cherrpy file session to improve file locking.
    """

    def acquire_lock(self, path=None):
        """Acquire an exclusive lock on the currently-loaded session data."""
        if path is None:
            path = self._get_file_path()
        path += self.LOCK_SUFFIX
        checker = locking.LockChecker(self.id, self.lock_timeout)
        while not checker.expired():
            try:
                self.lock = zc.lockfile.LockFile(path)
            except zc.lockfile.LockError:
                # Sleep for 1ms only.
                time.sleep(0.001)
            else:
                break
        self.locked = True
        if self.debug:
            cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')


@contextmanager
def session_lock():
    """
    Acquire session lock as required. Support re-intrant lock.
    """
    s = cherrypy.serving.session
    if s.locking == 'explicit' and not s.locked:
        s.acquire_lock()
        try:
            yield s
        finally:
            # When explicit, we want to save the session (with also release the lock.)
            if s.locking == 'explicit':
                s.save()
                cherrypy.serving.request._sessionsaved = True
    else:
        yield s
