/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */

#include "qpid/log/Statement.h"
#include "qpid/sys/windows/check.h"
#include "SCM.h"

#pragma comment(lib, "advapi32.lib")

namespace {

// Container that will close a SC_HANDLE upon destruction.
class AutoServiceHandle {
public:
    AutoServiceHandle(SC_HANDLE h_ = NULL) : h(h_) {}
    ~AutoServiceHandle() { if (h != NULL) ::CloseServiceHandle(h); }
    void release() { h = NULL; }
    void reset(SC_HANDLE newHandle)
    {
        if (h != NULL)
            ::CloseServiceHandle(h);
        h = newHandle;
    }
    operator SC_HANDLE() const { return h; }

private:
    SC_HANDLE h;
};

}

namespace qpid {
namespace windows {

SCM::SCM() : scmHandle(NULL)
{
}

SCM::~SCM()
{
    if (NULL != scmHandle)
        ::CloseServiceHandle(scmHandle);
}

/**
  * Install this executable as a service
  */
void SCM::install(const string& serviceName,
                  const string& serviceDesc,
                  const string& args,
                  DWORD startType,
                  const string& account,
                  const string& password,
                  const string& depends)
{
    // Handle dependent service name list; Windows wants a set of nul-separated
    // names ending with a double nul.
    string depends2 = depends;
    if (!depends2.empty()) {
        // CDL to null delimiter w/ trailing double null
        size_t p = 0;
        while ((p = depends2.find_first_of( ',', p)) != string::npos)
            depends2.replace(p, 1, 1, '\0');
        depends2.push_back('\0');
        depends2.push_back('\0');
    }

#if 0
    // I'm nervous about adding a user/password check here. Is this a
    // potential attack vector, letting users check passwords without
    // control?   -Steve Huston, Feb 24, 2011

    // Validate account, password
    HANDLE hToken = NULL;
    bool logStatus = false;
    if (!account.empty() && !password.empty() &&
        !(logStatus = ::LogonUserA(account.c_str(),
                                   "",
                                   password.c_str(),
                                   LOGON32_LOGON_NETWORK,
                                   LOGON32_PROVIDER_DEFAULT,
                                   &hToken ) != 0))
        std::cout << "warning: supplied account & password failed with LogonUser." << std::endl;
    if (logStatus)
        ::CloseHandle(hToken);
#endif

    // Get fully qualified .exe name
    char myPath[MAX_PATH];
    DWORD myPathLength = ::GetModuleFileName(NULL, myPath, MAX_PATH);
    QPID_WINDOWS_CHECK_NOT(myPathLength, 0);
    string imagePath(myPath, myPathLength);
    if (!args.empty())
        imagePath += " " + args;

    // Ensure there's a handle to the SCM database.
    openSvcManager();

    // Create the service
    SC_HANDLE svcHandle;
    svcHandle = ::CreateService(scmHandle,                 // SCM database
                                serviceName.c_str(),       // name of service
                                serviceDesc.c_str(),       // name to display
                                SERVICE_ALL_ACCESS,        // desired access
                                SERVICE_WIN32_OWN_PROCESS, // service type
                                startType,                 // start type
                                SERVICE_ERROR_NORMAL,      // error cntrl type
                                imagePath.c_str(),         // path to service's binary w/ optional arguments
                                NULL,                      // no load ordering group
                                NULL,                      // no tag identifier
                                depends2.empty() ? NULL : depends2.c_str(),
                                account.empty() ? NULL : account.c_str(), // account name, or NULL for LocalSystem
                                password.empty() ? NULL : password.c_str()); // password, or NULL for none
    QPID_WINDOWS_CHECK_NULL(svcHandle);
    ::CloseServiceHandle(svcHandle);
    QPID_LOG(info, "Service installed successfully");
}

/**
  *
  */
void SCM::uninstall(const string& serviceName)
{
    // Ensure there's a handle to the SCM database.
    openSvcManager();
    AutoServiceHandle svc(::OpenService(scmHandle,
                                        serviceName.c_str(),
                                        DELETE));
    QPID_WINDOWS_CHECK_NULL((SC_HANDLE)svc);
    QPID_WINDOWS_CHECK_NOT(::DeleteService(svc), 0);
    QPID_LOG(info, "Service deleted successfully.");
}

/**
  * Attempt to start the service.
  */
void SCM::start(const string& serviceName)
{
    // Ensure we have a handle to the SCM database.
    openSvcManager();

    // Get a handle to the service.
    AutoServiceHandle svc(::OpenService(scmHandle,
                                        serviceName.c_str(),
                                        SERVICE_ALL_ACCESS));
    QPID_WINDOWS_CHECK_NULL(svc);

    // Check the status in case the service is not stopped.
    DWORD state = waitForStateChangeFrom(svc, SERVICE_STOP_PENDING);
    if (state == SERVICE_STOP_PENDING)
        throw qpid::Exception("Timed out waiting for running service to stop.");

    // Attempt to start the service.
    QPID_WINDOWS_CHECK_NOT(::StartService(svc, 0, NULL), 0);

    QPID_LOG(info, "Service start pending...");

    // Check the status until the service is no longer start pending.
    state = waitForStateChangeFrom(svc, SERVICE_START_PENDING);
    // Determine whether the service is running.
    if (state == SERVICE_RUNNING) {
        QPID_LOG(info, "Service started successfully");
    }
    else {
        throw qpid::Exception(QPID_MSG("Service not yet running; state now " << state));
    }
}

/**
  *
  */
void SCM::stop(const string& serviceName)
{
    // Ensure a handle to the SCM database.
    openSvcManager();

    // Get a handle to the service.
    AutoServiceHandle svc(::OpenService(scmHandle,
                                        serviceName.c_str(),
                                        SERVICE_STOP | SERVICE_QUERY_STATUS |
                                        SERVICE_ENUMERATE_DEPENDENTS));
    QPID_WINDOWS_CHECK_NULL(svc);

    // Make sure the service is not already stopped; if it's stop-pending,
    // wait for it to finalize.
    DWORD state = waitForStateChangeFrom(svc, SERVICE_STOP_PENDING);
    if (state == SERVICE_STOPPED) {
        QPID_LOG(info, "Service is already stopped");
        return;
    }

    // If the service is running, dependencies must be stopped first.
    std::auto_ptr<ENUM_SERVICE_STATUS> deps;
    DWORD numDeps = getDependentServices(svc, deps);
    for (DWORD i = 0; i < numDeps; i++)
        stop(deps.get()[i].lpServiceName);

    // Dependents stopped; send a stop code to the service.
    SERVICE_STATUS_PROCESS ssp;
    if (!::ControlService(svc, SERVICE_CONTROL_STOP, (LPSERVICE_STATUS)&ssp))
        throw qpid::Exception(QPID_MSG("Stopping " << serviceName << ": " <<
                                       qpid::sys::strError(::GetLastError())));

    // Wait for the service to stop.
    state = waitForStateChangeFrom(svc, SERVICE_STOP_PENDING);
    if (state == SERVICE_STOPPED)
        QPID_LOG(info, QPID_MSG("Service " << serviceName <<
                                " stopped successfully."));
}

/**
  *
  */
void SCM::openSvcManager()
{
    if (NULL != scmHandle)
        return;

    scmHandle = ::OpenSCManager(NULL,    // local computer
                                NULL,    // ServicesActive database
                                SC_MANAGER_ALL_ACCESS); // Rights
    QPID_WINDOWS_CHECK_NULL(scmHandle);
}

DWORD SCM::waitForStateChangeFrom(SC_HANDLE svc, DWORD originalState)
{
    SERVICE_STATUS_PROCESS ssStatus;
    DWORD bytesNeeded;
    DWORD waitTime;
    if (!::QueryServiceStatusEx(svc,                    // handle to service
                                SC_STATUS_PROCESS_INFO, // information level
                                (LPBYTE)&ssStatus,      // address of structure
                                sizeof(ssStatus),       // size of structure
                                &bytesNeeded))          // size needed if buffer is too small
        throw QPID_WINDOWS_ERROR(::GetLastError());

    // Save the tick count and initial checkpoint.
    DWORD startTickCount = ::GetTickCount();
    DWORD oldCheckPoint = ssStatus.dwCheckPoint;

    // Wait for the service to change out of the noted state.
    while (ssStatus.dwCurrentState == originalState) {
        // Do not wait longer than the wait hint. A good interval is
        // one-tenth of the wait hint but not less than 1 second
        // and not more than 10 seconds.
        waitTime = ssStatus.dwWaitHint / 10;
        if (waitTime < 1000)
            waitTime = 1000;
        else if (waitTime > 10000)
            waitTime = 10000;

        ::Sleep(waitTime);

        // Check the status until the service is no longer stop pending.
        if (!::QueryServiceStatusEx(svc,
                                    SC_STATUS_PROCESS_INFO,
                                    (LPBYTE) &ssStatus,
                                    sizeof(ssStatus),
                                    &bytesNeeded))
            throw QPID_WINDOWS_ERROR(::GetLastError());

        if (ssStatus.dwCheckPoint > oldCheckPoint) {
            // Continue to wait and check.
            startTickCount = ::GetTickCount();
            oldCheckPoint = ssStatus.dwCheckPoint;
        } else {
            if ((::GetTickCount() - startTickCount) > ssStatus.dwWaitHint)
                break;
        }
    }
    return ssStatus.dwCurrentState;
}

/**
  * Get the services that depend on @arg svc.  All dependent service info
  * is returned in an array of ENUM_SERVICE_STATUS structures via @arg deps.
  *
  * @retval The number of dependent services.
  */
DWORD SCM::getDependentServices(SC_HANDLE svc,
                                std::auto_ptr<ENUM_SERVICE_STATUS>& deps)
{
    DWORD bytesNeeded;
    DWORD numEntries;

    // Pass a zero-length buffer to get the required buffer size.
    if (::EnumDependentServices(svc,
                                SERVICE_ACTIVE, 
                                0,
                                0,
                                &bytesNeeded,
                                &numEntries)) {
        // If the Enum call succeeds, then there are no dependent
        // services, so do nothing.
        return 0;
    }

    if (::GetLastError() != ERROR_MORE_DATA)
        throw QPID_WINDOWS_ERROR((::GetLastError()));

    // Allocate a buffer for the dependencies.
    deps.reset((LPENUM_SERVICE_STATUS)(new char[bytesNeeded]));
    // Enumerate the dependencies.
    if (!::EnumDependentServices(svc,
                                 SERVICE_ACTIVE,
                                 deps.get(),
                                 bytesNeeded,
                                 &bytesNeeded,
                                 &numEntries))
        throw QPID_WINDOWS_ERROR((::GetLastError()));
    return numEntries;
}

} }   // namespace qpid::windows
