/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is mozilla.org code.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 2002
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Conrad Carlen <ccarlen@netscape.com>
 *   Brendan Eich <brendan@mozilla.org>
 *   Colin Blake <colin@theblakes.com>
 *   Javier Pedemonte <pedemont@us.ibm.com>
 *   Mats Palmgren <mats.palmgren@bredband.net>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include "nsProfileStringTypes.h"
#include "nsProfileLock.h"
#include "nsCOMPtr.h"

#if defined(XP_MACOSX)
#include <Carbon/Carbon.h>
#include <CoreFoundation/CoreFoundation.h>
#endif

#ifdef XP_UNIX
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include "prnetdb.h"
#include "prsystem.h"
#include "prprf.h"
#include "prenv.h"
#endif

#ifdef VMS
#include <rmsdef.h>
#endif

// **********************************************************************
// class nsProfileLock
//
// This code was moved from profile/src/nsProfileAccess.
// **********************************************************************

#if defined (XP_UNIX)
static bool sDisableSignalHandling = false;
#endif

nsProfileLock::nsProfileLock() :
    mHaveLock(false)
#if defined (XP_WIN)
    ,mLockFileHandle(INVALID_HANDLE_VALUE)
#elif defined (XP_OS2)
    ,mLockFileHandle(-1)
#elif defined (XP_UNIX)
    ,mPidLockFileName(nsnull)
    ,mLockFileDesc(-1)
#endif
{
#if defined (XP_UNIX)
    next = prev = this;
    sDisableSignalHandling = PR_GetEnv("MOZ_DISABLE_SIG_HANDLER") ? true : false;
#endif
}


nsProfileLock::nsProfileLock(nsProfileLock& src)
{
    *this = src;
}


nsProfileLock& nsProfileLock::operator=(nsProfileLock& rhs)
{
    Unlock();

    mHaveLock = rhs.mHaveLock;
    rhs.mHaveLock = false;

#if defined (XP_WIN)
    mLockFileHandle = rhs.mLockFileHandle;
    rhs.mLockFileHandle = INVALID_HANDLE_VALUE;
#elif defined (XP_OS2)
    mLockFileHandle = rhs.mLockFileHandle;
    rhs.mLockFileHandle = -1;
#elif defined (XP_UNIX)
    mLockFileDesc = rhs.mLockFileDesc;
    rhs.mLockFileDesc = -1;
    mPidLockFileName = rhs.mPidLockFileName;
    rhs.mPidLockFileName = nsnull;
    if (mPidLockFileName)
    {
        // rhs had a symlink lock, therefore it was on the list.
        PR_REMOVE_LINK(&rhs);
        PR_APPEND_LINK(this, &mPidLockList);
    }
#endif

    return *this;
}


nsProfileLock::~nsProfileLock()
{
    Unlock();
}


#if defined (XP_UNIX)

static int setupPidLockCleanup;

PRCList nsProfileLock::mPidLockList =
    PR_INIT_STATIC_CLIST(&nsProfileLock::mPidLockList);

void nsProfileLock::RemovePidLockFiles(bool aFatalSignal)
{
    while (!PR_CLIST_IS_EMPTY(&mPidLockList))
    {
        nsProfileLock *lock = static_cast<nsProfileLock*>(mPidLockList.next);
        lock->Unlock(aFatalSignal);
    }
}

static struct sigaction SIGHUP_oldact;
static struct sigaction SIGINT_oldact;
static struct sigaction SIGQUIT_oldact;
static struct sigaction SIGILL_oldact;
static struct sigaction SIGABRT_oldact;
static struct sigaction SIGSEGV_oldact;
static struct sigaction SIGTERM_oldact;

void nsProfileLock::FatalSignalHandler(int signo
#ifdef SA_SIGINFO
                                       , siginfo_t *info, void *context
#endif
                                       )
{
    // Remove any locks still held.
    RemovePidLockFiles(true);

    // Chain to the old handler, which may exit.
    struct sigaction *oldact = nsnull;

    switch (signo) {
      case SIGHUP:
        oldact = &SIGHUP_oldact;
        break;
      case SIGINT:
        oldact = &SIGINT_oldact;
        break;
      case SIGQUIT:
        oldact = &SIGQUIT_oldact;
        break;
      case SIGILL:
        oldact = &SIGILL_oldact;
        break;
      case SIGABRT:
        oldact = &SIGABRT_oldact;
        break;
      case SIGSEGV:
        oldact = &SIGSEGV_oldact;
        break;
      case SIGTERM:
        oldact = &SIGTERM_oldact;
        break;
      default:
        NS_NOTREACHED("bad signo");
        break;
    }

    if (oldact) {
        if (oldact->sa_handler == SIG_DFL) {
            // Make sure the default sig handler is executed
            // We need it to get Mozilla to dump core.
            sigaction(signo,oldact,NULL);

            // Now that we've restored the default handler, unmask the
            // signal and invoke it.

            sigset_t unblock_sigs;
            sigemptyset(&unblock_sigs);
            sigaddset(&unblock_sigs, signo);

            sigprocmask(SIG_UNBLOCK, &unblock_sigs, NULL);

            raise(signo);
        }
#ifdef SA_SIGINFO
        else if (oldact->sa_sigaction &&
                 (oldact->sa_flags & SA_SIGINFO) == SA_SIGINFO) {
            oldact->sa_sigaction(signo, info, context);
        }
#endif
        else if (oldact->sa_handler && oldact->sa_handler != SIG_IGN)
        {
            oldact->sa_handler(signo);
        }
    }

    // Backstop exit call, just in case.
    _exit(signo);
}

nsresult nsProfileLock::LockWithFcntl(const nsACString& lockFilePath)
{
    nsresult rv = NS_OK;

    mLockFileDesc = open(PromiseFlatCString(lockFilePath).get(),
                          O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (mLockFileDesc != -1)
    {
        struct flock lock;
        lock.l_start = 0;
        lock.l_len = 0; // len = 0 means entire file
        lock.l_type = F_WRLCK;
        lock.l_whence = SEEK_SET;

        // If fcntl(F_GETLK) fails then the server does not support/allow fcntl(),
        // return failure rather than access denied in this case so we fallback
        // to using a symlink lock, bug 303633.
        struct flock testlock = lock;
        if (fcntl(mLockFileDesc, F_GETLK, &testlock) == -1)
        {
            close(mLockFileDesc);
            mLockFileDesc = -1;
            rv = NS_ERROR_FAILURE;
        }
        else if (fcntl(mLockFileDesc, F_SETLK, &lock) == -1)
        {
            close(mLockFileDesc);
            mLockFileDesc = -1;

            // With OS X, on NFS, errno == ENOTSUP
            // XXX Check for that and return specific rv for it?
#ifdef DEBUG
            printf("fcntl(F_SETLK) failed. errno = %d\n", errno);
#endif
            if (errno == EAGAIN || errno == EACCES)
                rv = NS_ERROR_FILE_ACCESS_DENIED;
            else
                rv = NS_ERROR_FAILURE;
        }
        else
            mHaveLock = true;
    }
    else
    {
        NS_ERROR("Failed to open lock file.");
        rv = NS_ERROR_FAILURE;
    }
    return rv;
}

static bool IsSymlinkStaleLock(struct in_addr* aAddr, const char* aFileName,
                                 bool aHaveFcntlLock)
{
    // the link exists; see if it's from this machine, and if
    // so if the process is still active
    char buf[1024];
    int len = readlink(aFileName, buf, sizeof buf - 1);
    if (len > 0)
    {
        buf[len] = '\0';
        char *colon = strchr(buf, ':');
        if (colon)
        {
            *colon++ = '\0';
            unsigned long addr = inet_addr(buf);
            if (addr != (unsigned long) -1)
            {
                if (colon[0] == '+' && aHaveFcntlLock) {
                    // This lock was placed by a Firefox build which would have
                    // taken the fnctl lock, and we've already taken the fcntl lock,
                    // so the process that created this obsolete lock must be gone
                    return true;
                }
                    
                char *after = nsnull;
                pid_t pid = strtol(colon, &after, 0);
                if (pid != 0 && *after == '\0')
                {
                    if (addr != aAddr->s_addr)
                    {
                        // Remote lock: give up even if stuck.
                        return false;
                    }
    
                    // kill(pid,0) is a neat trick to check if a
                    // process exists
                    if (kill(pid, 0) == 0 || errno != ESRCH)
                    {
                        // Local process appears to be alive, ass-u-me it
                        // is another Mozilla instance, or a compatible
                        // derivative, that's currently using the profile.
                        // XXX need an "are you Mozilla?" protocol
                        return false;
                    }
                }
            }
        }
    }
    return true;
}

nsresult nsProfileLock::LockWithSymlink(const nsACString& lockFilePath, bool aHaveFcntlLock)
{
    nsresult rv;

    struct in_addr inaddr;
    inaddr.s_addr = htonl(INADDR_LOOPBACK);

    char hostname[256];
    PRStatus status = PR_GetSystemInfo(PR_SI_HOSTNAME, hostname, sizeof hostname);
    if (status == PR_SUCCESS)
    {
        char netdbbuf[PR_NETDB_BUF_SIZE];
        PRHostEnt hostent;
        status = PR_GetHostByName(hostname, netdbbuf, sizeof netdbbuf, &hostent);
        if (status == PR_SUCCESS)
            memcpy(&inaddr, hostent.h_addr, sizeof inaddr);
    }

    char *signature =
        PR_smprintf("%s:%s%lu", inet_ntoa(inaddr), aHaveFcntlLock ? "+" : "",
                    (unsigned long)getpid());
    const nsPromiseFlatCString& flat = PromiseFlatCString(lockFilePath);
    const char *fileName = flat.get();
    int symlink_rv, symlink_errno = 0, tries = 0;

    // use ns4.x-compatible symlinks if the FS supports them
    while ((symlink_rv = symlink(signature, fileName)) < 0)
    {
        symlink_errno = errno;
        if (symlink_errno != EEXIST)
            break;

        if (!IsSymlinkStaleLock(&inaddr, fileName, aHaveFcntlLock))
            break;

        // Lock seems to be bogus: try to claim it.  Give up after a large
        // number of attempts (100 comes from the 4.x codebase).
        (void) unlink(fileName);
        if (++tries > 100)
            break;
    }

    PR_smprintf_free(signature);
    signature = nsnull;

    if (symlink_rv == 0)
    {
        // We exclusively created the symlink: record its name for eventual
        // unlock-via-unlink.
        rv = NS_OK;
        mHaveLock = true;
        mPidLockFileName = strdup(fileName);
        if (mPidLockFileName)
        {
            PR_APPEND_LINK(this, &mPidLockList);
            if (!setupPidLockCleanup++)
            {
                // Clean up on normal termination.
                // This instanciates a dummy class, and will trigger the class
                // destructor when libxul is unloaded. This is equivalent to atexit(),
                // but gracefully handles dlclose().
                static RemovePidLockFilesExiting r;

                // Clean up on abnormal termination, using POSIX sigaction.
                // Don't arm a handler if the signal is being ignored, e.g.,
                // because mozilla is run via nohup.
                if (!sDisableSignalHandling) {
                    struct sigaction act, oldact;
#ifdef SA_SIGINFO
                    act.sa_sigaction = FatalSignalHandler;
                    act.sa_flags = SA_SIGINFO;
#else
                    act.sa_handler = FatalSignalHandler;
#endif
                    sigfillset(&act.sa_mask);

#define CATCH_SIGNAL(signame)                                           \
PR_BEGIN_MACRO                                                          \
  if (sigaction(signame, NULL, &oldact) == 0 &&                         \
      oldact.sa_handler != SIG_IGN)                                     \
  {                                                                     \
      sigaction(signame, &act, &signame##_oldact);                      \
  }                                                                     \
  PR_END_MACRO

                    CATCH_SIGNAL(SIGHUP);
                    CATCH_SIGNAL(SIGINT);
                    CATCH_SIGNAL(SIGQUIT);
                    CATCH_SIGNAL(SIGILL);
                    CATCH_SIGNAL(SIGABRT);
                    CATCH_SIGNAL(SIGSEGV);
                    CATCH_SIGNAL(SIGTERM);

#undef CATCH_SIGNAL
                }
            }
        }
    }
    else if (symlink_errno == EEXIST)
        rv = NS_ERROR_FILE_ACCESS_DENIED;
    else
    {
#ifdef DEBUG
        printf("symlink() failed. errno = %d\n", errno);
#endif
        rv = NS_ERROR_FAILURE;
    }
    return rv;
}
#endif /* XP_UNIX */

nsresult nsProfileLock::Lock(nsILocalFile* aProfileDir,
                             nsIProfileUnlocker* *aUnlocker)
{
#if defined (XP_MACOSX)
    NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, ".parentlock");
    NS_NAMED_LITERAL_STRING(OLD_LOCKFILE_NAME, "parent.lock");
#elif defined (XP_UNIX)
    NS_NAMED_LITERAL_STRING(OLD_LOCKFILE_NAME, "lock");
    NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, ".parentlock");
#else
    NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, "parent.lock");
#endif

    nsresult rv;
    if (aUnlocker)
        *aUnlocker = nsnull;

    NS_ENSURE_STATE(!mHaveLock);

    bool isDir;
    rv = aProfileDir->IsDirectory(&isDir);
    if (NS_FAILED(rv))
        return rv;
    if (!isDir)
        return NS_ERROR_FILE_NOT_DIRECTORY;

    nsCOMPtr<nsILocalFile> lockFile;
    rv = aProfileDir->Clone((nsIFile **)((void **)getter_AddRefs(lockFile)));
    if (NS_FAILED(rv))
        return rv;

    rv = lockFile->Append(LOCKFILE_NAME);
    if (NS_FAILED(rv))
        return rv;
        
#if defined(XP_MACOSX)
    // First, try locking using fcntl. It is more reliable on
    // a local machine, but may not be supported by an NFS server.
    nsCAutoString filePath;
    rv = lockFile->GetNativePath(filePath);
    if (NS_FAILED(rv))
        return rv;

    rv = LockWithFcntl(filePath);
    if (NS_FAILED(rv) && (rv != NS_ERROR_FILE_ACCESS_DENIED))
    {
        // If that failed for any reason other than NS_ERROR_FILE_ACCESS_DENIED,
        // assume we tried an NFS that does not support it. Now, try with symlink.
        rv = LockWithSymlink(filePath, false);
    }
    
    if (NS_SUCCEEDED(rv))
    {
        // Check for the old-style lock used by pre-mozilla 1.3 builds.
        // Those builds used an earlier check to prevent the application
        // from launching if another instance was already running. Because
        // of that, we don't need to create an old-style lock as well.
        struct LockProcessInfo
        {
            ProcessSerialNumber psn;
            unsigned long launchDate;
        };

        PRFileDesc *fd = nsnull;
        PRInt32 ioBytes;
        ProcessInfoRec processInfo;
        LockProcessInfo lockProcessInfo;

        rv = lockFile->SetLeafName(OLD_LOCKFILE_NAME);
        if (NS_FAILED(rv))
            return rv;
        rv = lockFile->OpenNSPRFileDesc(PR_RDONLY, 0, &fd);
        if (NS_SUCCEEDED(rv))
        {
            ioBytes = PR_Read(fd, &lockProcessInfo, sizeof(LockProcessInfo));
            PR_Close(fd);

            if (ioBytes == sizeof(LockProcessInfo))
            {
#ifdef __LP64__
                processInfo.processAppRef = NULL;
#else
                processInfo.processAppSpec = NULL;
#endif
                processInfo.processName = NULL;
                processInfo.processInfoLength = sizeof(ProcessInfoRec);
                if (::GetProcessInformation(&lockProcessInfo.psn, &processInfo) == noErr &&
                    processInfo.processLaunchDate == lockProcessInfo.launchDate)
                {
                    return NS_ERROR_FILE_ACCESS_DENIED;
                }
            }
            else
            {
                NS_WARNING("Could not read lock file - ignoring lock");
            }
        }
        rv = NS_OK; // Don't propagate error from OpenNSPRFileDesc.
    }
#elif defined(XP_UNIX)
    nsCAutoString filePath;
    rv = lockFile->GetNativePath(filePath);
    if (NS_FAILED(rv))
        return rv;

    // Get the old lockfile name
    nsCOMPtr<nsIFile> oldLockFile;
    rv = aProfileDir->Clone(getter_AddRefs(oldLockFile));
    if (NS_FAILED(rv))
        return rv;
    rv = oldLockFile->Append(OLD_LOCKFILE_NAME);
    if (NS_FAILED(rv))
        return rv;
    nsCAutoString oldFilePath;
    rv = oldLockFile->GetNativePath(oldFilePath);
    if (NS_FAILED(rv))
        return rv;

    // First, try locking using fcntl. It is more reliable on
    // a local machine, but may not be supported by an NFS server.
    rv = LockWithFcntl(filePath);
    if (NS_SUCCEEDED(rv)) {
        // Check to see whether there is a symlink lock held by an older
        // Firefox build, and also place our own symlink lock --- but
        // mark it "obsolete" so that other newer builds can break the lock
        // if they obtain the fcntl lock
        rv = LockWithSymlink(oldFilePath, true);

        // If the symlink failed for some reason other than it already
        // exists, then something went wrong e.g. the file system
        // doesn't support symlinks, or we don't have permission to
        // create a symlink there.  In such cases we should just
        // continue because it's unlikely there is an old build
        // running with a symlink there and we've already successfully
        // placed a fcntl lock.
        if (rv != NS_ERROR_FILE_ACCESS_DENIED)
            rv = NS_OK;
    }
    else if (rv != NS_ERROR_FILE_ACCESS_DENIED)
    {
        // If that failed for any reason other than NS_ERROR_FILE_ACCESS_DENIED,
        // assume we tried an NFS that does not support it. Now, try with symlink
        // using the old symlink path
        rv = LockWithSymlink(oldFilePath, false);
    }

#elif defined(XP_WIN)
    nsAutoString filePath;
    rv = lockFile->GetPath(filePath);
    if (NS_FAILED(rv))
        return rv;

    mLockFileHandle = CreateFileW(filePath.get(),
                                  GENERIC_READ | GENERIC_WRITE,
                                  0, // no sharing - of course
                                  nsnull,
                                  OPEN_ALWAYS,
                                  FILE_FLAG_DELETE_ON_CLOSE,
                                  nsnull);
    if (mLockFileHandle == INVALID_HANDLE_VALUE) {
        // XXXbsmedberg: provide a profile-unlocker here!
        return NS_ERROR_FILE_ACCESS_DENIED;
    }
#elif defined(XP_OS2)
    nsCAutoString filePath;
    rv = lockFile->GetNativePath(filePath);
    if (NS_FAILED(rv))
        return rv;

    ULONG   ulAction = 0;
    APIRET  rc;
    rc = DosOpen(filePath.get(),
                  &mLockFileHandle,
                  &ulAction,
                  0,
                  FILE_NORMAL,
                  OPEN_ACTION_CREATE_IF_NEW | OPEN_ACTION_OPEN_IF_EXISTS,
                  OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYREADWRITE | OPEN_FLAGS_NOINHERIT,
                  0 );
    if (rc != NO_ERROR)
    {
        mLockFileHandle = -1;
        return NS_ERROR_FILE_ACCESS_DENIED;
    }
#elif defined(VMS)
    nsCAutoString filePath;
    rv = lockFile->GetNativePath(filePath);
    if (NS_FAILED(rv))
        return rv;

    mLockFileDesc = open_noshr(filePath.get(), O_CREAT, 0666);
    if (mLockFileDesc == -1)
    {
        if ((errno == EVMSERR) && (vaxc$errno == RMS$_FLK))
        {
            return NS_ERROR_FILE_ACCESS_DENIED;
        }
        else
        {
            NS_ERROR("Failed to open lock file.");
            return NS_ERROR_FAILURE;
        }
    }
#endif

    mHaveLock = true;

    return rv;
}


nsresult nsProfileLock::Unlock(bool aFatalSignal)
{
    nsresult rv = NS_OK;

    if (mHaveLock)
    {
#if defined (XP_WIN)
        if (mLockFileHandle != INVALID_HANDLE_VALUE)
        {
            CloseHandle(mLockFileHandle);
            mLockFileHandle = INVALID_HANDLE_VALUE;
        }
#elif defined (XP_OS2)
        if (mLockFileHandle != -1)
        {
            DosClose(mLockFileHandle);
            mLockFileHandle = -1;
        }
#elif defined (XP_UNIX)
        if (mPidLockFileName)
        {
            PR_REMOVE_LINK(this);
            (void) unlink(mPidLockFileName);

            // Only free mPidLockFileName if we're not in the fatal signal
            // handler.  The problem is that a call to free() might be the
            // cause of this fatal signal.  If so, calling free() might cause
            // us to wait on the malloc implementation's lock.  We're already
            // holding this lock, so we'll deadlock. See bug 522332.
            if (!aFatalSignal)
                free(mPidLockFileName);
            mPidLockFileName = nsnull;
        }
        else if (mLockFileDesc != -1)
        {
            close(mLockFileDesc);
            mLockFileDesc = -1;
            // Don't remove it
        }
#endif

        mHaveLock = false;
    }

    return rv;
}
