/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <windows.h>
#include <aclapi.h>
#include <shlwapi.h>

// Used for DNLEN and UNLEN
#include <lm.h>

#include <nsWindowsHelpers.h>
#include "mozilla/UniquePtr.h"

#include "serviceinstall.h"
#include "servicebase.h"
#include "updatehelper.h"
#include "shellapi.h"
#include "readstrings.h"
#include "updatererrors.h"
#include "commonupdatedir.h"

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

// This uninstall key is defined originally in maintenanceservice_installer.nsi
#define MAINT_UNINSTALL_KEY                                                    \
  L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MozillaMaintenan" \
  L"ceService"

static BOOL UpdateUninstallerVersionString(LPWSTR versionString) {
  HKEY uninstallKey;
  if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, MAINT_UNINSTALL_KEY, 0,
                    KEY_WRITE | KEY_WOW64_32KEY,
                    &uninstallKey) != ERROR_SUCCESS) {
    return FALSE;
  }

  LONG rv = RegSetValueExW(uninstallKey, L"DisplayVersion", 0, REG_SZ,
                           reinterpret_cast<const BYTE*>(versionString),
                           (wcslen(versionString) + 1) * sizeof(WCHAR));
  RegCloseKey(uninstallKey);
  return rv == ERROR_SUCCESS;
}

/**
 * A wrapper function to read strings for the maintenance service.
 *
 * @param path    The path of the ini file to read from
 * @param results The maintenance service strings that were read
 * @return OK on success
 */
static int ReadMaintenanceServiceStrings(
    LPCWSTR path, MaintenanceServiceStringTable* results) {
  // Read in the maintenance service description string if specified.
  const unsigned int kNumStrings = 1;
  const char* kServiceKeys = "MozillaMaintenanceDescription\0";
  mozilla::UniquePtr<char[]> serviceString;
  int result = ReadStrings(path, kServiceKeys, kNumStrings, &serviceString);
  if (result != OK) {
    results->serviceDescription = mozilla::MakeUnique<char[]>(1);
    results->serviceDescription.get()[0] = '\0';
  }
  results->serviceDescription.swap(serviceString);
  return result;
}

/**
 * Obtains the version number from the specified PE file's version information
 * Version Format: A.B.C.D (Example 10.0.0.300)
 *
 * @param  path The path of the file to check the version on
 * @param  A    The first part of the version number
 * @param  B    The second part of the version number
 * @param  C    The third part of the version number
 * @param  D    The fourth part of the version number
 * @return TRUE if successful
 */
static BOOL GetVersionNumberFromPath(LPWSTR path, DWORD& A, DWORD& B, DWORD& C,
                                     DWORD& D) {
  DWORD fileVersionInfoSize = GetFileVersionInfoSizeW(path, 0);
  mozilla::UniquePtr<char[]> fileVersionInfo(new char[fileVersionInfoSize]);
  if (!GetFileVersionInfoW(path, 0, fileVersionInfoSize,
                           fileVersionInfo.get())) {
    LOG_WARN(
        ("Could not obtain file info of old service.  (%lu)", GetLastError()));
    return FALSE;
  }

  VS_FIXEDFILEINFO* fixedFileInfo =
      reinterpret_cast<VS_FIXEDFILEINFO*>(fileVersionInfo.get());
  UINT size;
  if (!VerQueryValueW(fileVersionInfo.get(), L"\\",
                      reinterpret_cast<LPVOID*>(&fixedFileInfo), &size)) {
    LOG_WARN(("Could not query file version info of old service.  (%lu)",
              GetLastError()));
    return FALSE;
  }

  A = HIWORD(fixedFileInfo->dwFileVersionMS);
  B = LOWORD(fixedFileInfo->dwFileVersionMS);
  C = HIWORD(fixedFileInfo->dwFileVersionLS);
  D = LOWORD(fixedFileInfo->dwFileVersionLS);
  return TRUE;
}

/**
 * Updates the service description with what is stored in updater.ini
 * at the same path as the currently executing module binary.
 *
 * @param serviceHandle A handle to an opened service with
 *                      SERVICE_CHANGE_CONFIG access right
 * @param TRUE on succcess.
 */
BOOL UpdateServiceDescription(SC_HANDLE serviceHandle) {
  WCHAR updaterINIPath[MAX_PATH + 1];
  if (!GetModuleFileNameW(nullptr, updaterINIPath,
                          sizeof(updaterINIPath) / sizeof(updaterINIPath[0]))) {
    LOG_WARN(
        ("Could not obtain module filename when attempting to "
         "modify service description.  (%lu)",
         GetLastError()));
    return FALSE;
  }

  if (!PathRemoveFileSpecW(updaterINIPath)) {
    LOG_WARN(
        ("Could not remove file spec when attempting to "
         "modify service description.  (%lu)",
         GetLastError()));
    return FALSE;
  }

  if (!PathAppendSafe(updaterINIPath, L"updater.ini")) {
    LOG_WARN(
        ("Could not append updater.ini filename when attempting to "
         "modify service description.  (%lu)",
         GetLastError()));
    return FALSE;
  }

  if (GetFileAttributesW(updaterINIPath) == INVALID_FILE_ATTRIBUTES) {
    LOG_WARN(
        ("updater.ini file does not exist, will not modify "
         "service description.  (%lu)",
         GetLastError()));
    return FALSE;
  }

  MaintenanceServiceStringTable serviceStrings;
  int rv = ReadMaintenanceServiceStrings(updaterINIPath, &serviceStrings);
  if (rv != OK || !strlen(serviceStrings.serviceDescription.get())) {
    LOG_WARN(
        ("updater.ini file does not contain a maintenance "
         "service description."));
    return FALSE;
  }

  int bufferSize = MultiByteToWideChar(
      CP_UTF8, 0, serviceStrings.serviceDescription.get(), -1, nullptr, 0);
  mozilla::UniquePtr<WCHAR[]> serviceDescription =
      mozilla::MakeUnique<WCHAR[]>(bufferSize);
  if (!MultiByteToWideChar(CP_UTF8, 0, serviceStrings.serviceDescription.get(),
                           -1, serviceDescription.get(), bufferSize)) {
    LOG_WARN(("Could not convert description to wide string format.  (%lu)",
              GetLastError()));
    return FALSE;
  }

  SERVICE_DESCRIPTIONW descriptionConfig;
  descriptionConfig.lpDescription = serviceDescription.get();
  if (!ChangeServiceConfig2W(serviceHandle, SERVICE_CONFIG_DESCRIPTION,
                             &descriptionConfig)) {
    LOG_WARN(("Could not change service config.  (%lu)", GetLastError()));
    return FALSE;
  }

  LOG(("The service description was updated successfully."));
  return TRUE;
}

/**
 * Determines if the MozillaMaintenance service path needs to be updated
 * and fixes it if it is wrong.
 *
 * @param service             A handle to the service to fix.
 * @param currentServicePath  The current (possibly wrong) path that is used.
 * @param servicePathWasWrong Out parameter set to TRUE if a fix was needed.
 * @return TRUE if the service path is now correct.
 */
BOOL FixServicePath(SC_HANDLE service, LPCWSTR currentServicePath,
                    BOOL& servicePathWasWrong) {
  // When we originally upgraded the MozillaMaintenance service we
  // would uninstall the service on each upgrade.  This had an
  // intermittent error which could cause the service to use the file
  // maintenanceservice_tmp.exe as the install path.  Only a small number
  // of Nightly users would be affected by this, but we check for this
  // state here and fix the user if they are affected.
  //
  // We also fix the path in the case of the path not being quoted.
  size_t currentServicePathLen = wcslen(currentServicePath);
  bool doesServiceHaveCorrectPath =
      currentServicePathLen > 2 &&
      !wcsstr(currentServicePath, L"maintenanceservice_tmp.exe") &&
      currentServicePath[0] == L'\"' &&
      currentServicePath[currentServicePathLen - 1] == L'\"';

  if (doesServiceHaveCorrectPath) {
    LOG(("The MozillaMaintenance service path is correct."));
    servicePathWasWrong = FALSE;
    return TRUE;
  }
  // This is a recoverable situation so not logging as a warning
  LOG(("The MozillaMaintenance path is NOT correct. It was: %ls",
       currentServicePath));

  servicePathWasWrong = TRUE;
  WCHAR fixedPath[MAX_PATH + 1] = {L'\0'};
  wcsncpy(fixedPath, currentServicePath, MAX_PATH);
  PathUnquoteSpacesW(fixedPath);
  if (!PathRemoveFileSpecW(fixedPath)) {
    LOG_WARN(("Couldn't remove file spec.  (%lu)", GetLastError()));
    return FALSE;
  }
  if (!PathAppendSafe(fixedPath, L"maintenanceservice.exe")) {
    LOG_WARN(("Couldn't append file spec.  (%lu)", GetLastError()));
    return FALSE;
  }
  PathQuoteSpacesW(fixedPath);

  if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
                            SERVICE_NO_CHANGE, fixedPath, nullptr, nullptr,
                            nullptr, nullptr, nullptr, nullptr)) {
    LOG_WARN(("Could not fix service path.  (%lu)", GetLastError()));
    return FALSE;
  }

  LOG(("Fixed service path to: %ls.", fixedPath));
  return TRUE;
}

/**
 * Installs or upgrades the SVC_NAME service.
 * If an existing service is already installed, we replace it with the
 * currently running process.
 *
 * @param  action The action to perform.
 * @return TRUE if the service was installed/upgraded
 */
BOOL SvcInstall(SvcInstallAction action) {
  // Get a handle to the local computer SCM database with full access rights.
  nsAutoServiceHandle schSCManager(
      OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS));
  if (!schSCManager) {
    LOG_WARN(("Could not open service manager.  (%lu)", GetLastError()));
    return FALSE;
  }

  WCHAR newServiceBinaryPath[MAX_PATH + 1];
  if (!GetModuleFileNameW(
          nullptr, newServiceBinaryPath,
          sizeof(newServiceBinaryPath) / sizeof(newServiceBinaryPath[0]))) {
    LOG_WARN(
        ("Could not obtain module filename when attempting to "
         "install service.  (%lu)",
         GetLastError()));
    return FALSE;
  }

  // Check if we already have the service installed.
  nsAutoServiceHandle schService(
      OpenServiceW(schSCManager, SVC_NAME, SERVICE_ALL_ACCESS));
  DWORD lastError = GetLastError();
  if (!schService && ERROR_SERVICE_DOES_NOT_EXIST != lastError) {
    // The service exists but we couldn't open it
    LOG_WARN(("Could not open service.  (%lu)", GetLastError()));
    return FALSE;
  }

  if (schService) {
    // The service exists but it may not have the correct permissions.
    // This could happen if the permissions were not set correctly originally
    // or have been changed after the installation.  This will reset the
    // permissions back to allow limited user accounts.
    if (!SetUserAccessServiceDACL(schService)) {
      LOG_WARN(
          ("Could not reset security ACE on service handle. It might not be "
           "possible to start the service. This error should never "
           "happen.  (%lu)",
           GetLastError()));
    }

    // The service exists and we opened it
    DWORD bytesNeeded;
    if (!QueryServiceConfigW(schService, nullptr, 0, &bytesNeeded) &&
        GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
      LOG_WARN(
          ("Could not determine buffer size for query service config.  (%lu)",
           GetLastError()));
      return FALSE;
    }

    // Get the service config information, in particular we want the binary
    // path of the service.
    mozilla::UniquePtr<char[]> serviceConfigBuffer(new char[bytesNeeded]);
    if (!QueryServiceConfigW(
            schService,
            reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()),
            bytesNeeded, &bytesNeeded)) {
      LOG_WARN(("Could open service but could not query service config.  (%lu)",
                GetLastError()));
      return FALSE;
    }
    QUERY_SERVICE_CONFIGW& serviceConfig =
        *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get());

    // Check if we need to fix the service path
    BOOL servicePathWasWrong;
    static BOOL alreadyCheckedFixServicePath = FALSE;
    if (!alreadyCheckedFixServicePath) {
      if (!FixServicePath(schService, serviceConfig.lpBinaryPathName,
                          servicePathWasWrong)) {
        LOG_WARN(
            ("Could not fix service path. This should never happen.  (%lu)",
             GetLastError()));
        // True is returned because the service is pointing to
        // maintenanceservice_tmp.exe so it actually was upgraded to the
        // newest installed service.
        return TRUE;
      } else if (servicePathWasWrong) {
        // Now that the path is fixed we should re-attempt the install.
        // This current process' image path is maintenanceservice_tmp.exe.
        // The service used to point to maintenanceservice_tmp.exe.
        // The service was just fixed to point to maintenanceservice.exe.
        // Re-attempting an install from scratch will work as normal.
        alreadyCheckedFixServicePath = TRUE;
        LOG(("Restarting install action: %d", action));
        return SvcInstall(action);
      }
    }

    // Ensure the service path is not quoted. We own this memory and know it to
    // be large enough for the quoted path, so it is large enough for the
    // unquoted path.  This function cannot fail.
    PathUnquoteSpacesW(serviceConfig.lpBinaryPathName);

    // Obtain the existing maintenanceservice file's version number and
    // the new file's version number.  Versions are in the format of
    // A.B.C.D.
    DWORD existingA, existingB, existingC, existingD;
    DWORD newA, newB, newC, newD;
    BOOL obtainedExistingVersionInfo =
        GetVersionNumberFromPath(serviceConfig.lpBinaryPathName, existingA,
                                 existingB, existingC, existingD);
    if (!GetVersionNumberFromPath(newServiceBinaryPath, newA, newB, newC,
                                  newD)) {
      LOG_WARN(("Could not obtain version number from new path"));
      return FALSE;
    }

    // Check if we need to replace the old binary with the new one
    // If we couldn't get the old version info then we assume we should
    // replace it.
    if (ForceInstallSvc == action || !obtainedExistingVersionInfo ||
        (existingA < newA) || (existingA == newA && existingB < newB) ||
        (existingA == newA && existingB == newB && existingC < newC) ||
        (existingA == newA && existingB == newB && existingC == newC &&
         existingD < newD)) {
      // We have a newer updater, so update the description from the INI file.
      UpdateServiceDescription(schService);

      schService.reset();
      if (!StopService()) {
        return FALSE;
      }

      if (!wcscmp(newServiceBinaryPath, serviceConfig.lpBinaryPathName)) {
        LOG(
            ("File is already in the correct location, no action needed for "
             "upgrade.  The path is: \"%ls\"",
             newServiceBinaryPath));
        return TRUE;
      }

      BOOL result = TRUE;

      // Attempt to copy the new binary over top the existing binary.
      // If there is an error we try to move it out of the way and then
      // copy it in.  First try the safest / easiest way to overwrite the file.
      if (!CopyFileW(newServiceBinaryPath, serviceConfig.lpBinaryPathName,
                     FALSE)) {
        LOG_WARN(
            ("Could not overwrite old service binary file. "
             "This should never happen, but if it does the next "
             "upgrade will fix it, the service is not a critical "
             "component that needs to be installed for upgrades "
             "to work.  (%lu)",
             GetLastError()));

        // We rename the last 3 filename chars in an unsafe way.  Manually
        // verify there are more than 3 chars for safe failure in MoveFileExW.
        const size_t len = wcslen(serviceConfig.lpBinaryPathName);
        if (len > 3) {
          // Calculate the temp file path that we're moving the file to. This
          // is the same as the proper service path but with a .old extension.
          LPWSTR oldServiceBinaryTempPath = new WCHAR[len + 1];
          memset(oldServiceBinaryTempPath, 0, (len + 1) * sizeof(WCHAR));
          wcsncpy(oldServiceBinaryTempPath, serviceConfig.lpBinaryPathName,
                  len);
          // Rename the last 3 chars to 'old'
          wcsncpy(oldServiceBinaryTempPath + len - 3, L"old", 3);

          // Move the current (old) service file to the temp path.
          if (MoveFileExW(serviceConfig.lpBinaryPathName,
                          oldServiceBinaryTempPath,
                          MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
            // The old binary is moved out of the way, copy in the new one.
            if (!CopyFileW(newServiceBinaryPath, serviceConfig.lpBinaryPathName,
                           FALSE)) {
              // It is best to leave the old service binary in this condition.
              LOG_WARN(
                  ("The new service binary could not be copied in."
                   " The service will not be upgraded."));
              result = FALSE;
            } else {
              LOG(
                  ("The new service binary was copied in by first moving the"
                   " old one out of the way."));
            }

            // Attempt to get rid of the old service temp path.
            if (DeleteFileW(oldServiceBinaryTempPath)) {
              LOG(("The old temp service path was deleted: %ls.",
                   oldServiceBinaryTempPath));
            } else {
              // The old temp path could not be removed.  It will be removed
              // the next time the user can't copy the binary in or on
              // uninstall.
              LOG_WARN(("The old temp service path was not deleted."));
            }
          } else {
            // It is best to leave the old service binary in this condition.
            LOG_WARN(
                ("Could not move old service file out of the way from:"
                 " \"%ls\" to \"%ls\". Service will not be upgraded.  (%lu)",
                 serviceConfig.lpBinaryPathName, oldServiceBinaryTempPath,
                 GetLastError()));
            result = FALSE;
          }
          delete[] oldServiceBinaryTempPath;
        } else {
          // It is best to leave the old service binary in this condition.
          LOG_WARN(
              ("Service binary path was less than 3, service will"
               " not be updated.  This should never happen."));
          result = FALSE;
        }
      } else {
        WCHAR versionStr[128] = {L'\0'};
        swprintf(versionStr, 128, L"%d.%d.%d.%d", newA, newB, newC, newD);
        if (!UpdateUninstallerVersionString(versionStr)) {
          LOG(("The uninstaller version string could not be updated."));
        }
        LOG(("The new service binary was copied in."));
      }

      // We made a copy of ourselves to the existing location.
      // The tmp file (the process of which we are executing right now) will be
      // left over.  Attempt to delete the file on the next reboot.
      if (MoveFileExW(newServiceBinaryPath, nullptr,
                      MOVEFILE_DELAY_UNTIL_REBOOT)) {
        LOG(("Deleting the old file path on the next reboot: %ls.",
             newServiceBinaryPath));
      } else {
        LOG_WARN(("Call to delete the old file path failed: %ls.",
                  newServiceBinaryPath));
      }

      return result;
    }

    // We don't need to copy ourselves to the existing location.
    // The tmp file (the process of which we are executing right now) will be
    // left over.  Attempt to delete the file on the next reboot.
    MoveFileExW(newServiceBinaryPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT);

    // nothing to do, we already have a newer service installed
    return TRUE;
  }

  // If the service does not exist and we are upgrading, don't install it.
  if (UpgradeSvc == action) {
    // The service does not exist and we are upgrading, so don't install it
    return TRUE;
  }

  // Quote the path only if it contains spaces.
  PathQuoteSpacesW(newServiceBinaryPath);
  // The service does not already exist so create the service as on demand
  schService.own(CreateServiceW(
      schSCManager, SVC_NAME, SVC_DISPLAY_NAME, SERVICE_ALL_ACCESS,
      SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
      newServiceBinaryPath, nullptr, nullptr, nullptr, nullptr, nullptr));
  if (!schService) {
    LOG_WARN(
        ("Could not create Windows service. "
         "This error should never happen since a service install "
         "should only be called when elevated.  (%lu)",
         GetLastError()));
    return FALSE;
  }

  if (!SetUserAccessServiceDACL(schService)) {
    LOG_WARN(
        ("Could not set security ACE on service handle, the service will not "
         "be able to be started from unelevated processes. "
         "This error should never happen.  (%lu)",
         GetLastError()));
  }

  UpdateServiceDescription(schService);

  return TRUE;
}

/**
 * Stops the Maintenance service.
 *
 * @return TRUE if successful.
 */
BOOL StopService() {
  // Get a handle to the local computer SCM database with full access rights.
  nsAutoServiceHandle schSCManager(
      OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS));
  if (!schSCManager) {
    LOG_WARN(("Could not open service manager.  (%lu)", GetLastError()));
    return FALSE;
  }

  // Open the service
  nsAutoServiceHandle schService(
      OpenServiceW(schSCManager, SVC_NAME, SERVICE_ALL_ACCESS));
  if (!schService) {
    LOG_WARN(("Could not open service.  (%lu)", GetLastError()));
    return FALSE;
  }

  LOG(("Sending stop request..."));
  SERVICE_STATUS status;
  SetLastError(ERROR_SUCCESS);
  if (!ControlService(schService, SERVICE_CONTROL_STOP, &status) &&
      GetLastError() != ERROR_SERVICE_NOT_ACTIVE) {
    LOG_WARN(("Error sending stop request.  (%lu)", GetLastError()));
  }

  schSCManager.reset();
  schService.reset();

  LOG(("Waiting for service stop..."));
  DWORD lastState = WaitForServiceStop(SVC_NAME, 30);

  // The service can be in a stopped state but the exe still in use
  // so make sure the process is really gone before proceeding
  WaitForProcessExit(L"maintenanceservice.exe", 30);
  LOG(("Done waiting for service stop, last service state: %lu", lastState));

  return lastState == SERVICE_STOPPED;
}

/**
 * Uninstalls the Maintenance service.
 *
 * @return TRUE if successful.
 */
BOOL SvcUninstall() {
  // Get a handle to the local computer SCM database with full access rights.
  nsAutoServiceHandle schSCManager(
      OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS));
  if (!schSCManager) {
    LOG_WARN(("Could not open service manager.  (%lu)", GetLastError()));
    return FALSE;
  }

  // Open the service
  nsAutoServiceHandle schService(
      OpenServiceW(schSCManager, SVC_NAME, SERVICE_ALL_ACCESS));
  if (!schService) {
    LOG_WARN(("Could not open service.  (%lu)", GetLastError()));
    return FALSE;
  }

  // Stop the service so it deletes faster and so the uninstaller
  // can actually delete its EXE.
  DWORD totalWaitTime = 0;
  SERVICE_STATUS status;
  static const int maxWaitTime = 1000 * 60;  // Never wait more than a minute
  if (ControlService(schService, SERVICE_CONTROL_STOP, &status)) {
    do {
      Sleep(status.dwWaitHint);
      totalWaitTime += (status.dwWaitHint + 10);
      if (status.dwCurrentState == SERVICE_STOPPED) {
        break;
      } else if (totalWaitTime > maxWaitTime) {
        break;
      }
    } while (QueryServiceStatus(schService, &status));
  }

  // Delete the service or mark it for deletion
  BOOL deleted = DeleteService(schService);
  if (!deleted) {
    deleted = (GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE);
  }

  return deleted;
}

/**
 * Sets the access control list for user access for the specified service.
 *
 * @param  hService The service to set the access control list on
 * @return TRUE if successful
 */
BOOL SetUserAccessServiceDACL(SC_HANDLE hService) {
  PACL pNewAcl = nullptr;
  PSECURITY_DESCRIPTOR psd = nullptr;
  DWORD lastError = SetUserAccessServiceDACL(hService, pNewAcl, psd);
  if (pNewAcl) {
    LocalFree((HLOCAL)pNewAcl);
  }
  if (psd) {
    LocalFree((LPVOID)psd);
  }
  return ERROR_SUCCESS == lastError;
}

/**
 * Sets the access control list for user access for the specified service.
 *
 * @param  hService  The service to set the access control list on
 * @param  pNewAcl   The out param ACL which should be freed by caller
 * @param  psd       out param security descriptor, should be freed by caller
 * @return ERROR_SUCCESS if successful
 */
DWORD
SetUserAccessServiceDACL(SC_HANDLE hService, PACL& pNewAcl,
                         PSECURITY_DESCRIPTOR psd) {
  // Get the current security descriptor needed size
  DWORD needed = 0;
  if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, &psd, 0,
                                  &needed)) {
    if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
      LOG_WARN(("Could not query service object security size.  (%lu)",
                GetLastError()));
      return GetLastError();
    }

    DWORD size = needed;
    psd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, size);
    if (!psd) {
      LOG_WARN(
          ("Could not allocate security descriptor.  (%lu)", GetLastError()));
      return ERROR_INSUFFICIENT_BUFFER;
    }

    // Get the actual security descriptor now
    if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, psd,
                                    size, &needed)) {
      LOG_WARN(
          ("Could not allocate security descriptor.  (%lu)", GetLastError()));
      return GetLastError();
    }
  }

  // Get the current DACL from the security descriptor.
  PACL pacl = nullptr;
  BOOL bDaclPresent = FALSE;
  BOOL bDaclDefaulted = FALSE;
  if (!GetSecurityDescriptorDacl(psd, &bDaclPresent, &pacl, &bDaclDefaulted)) {
    LOG_WARN(("Could not obtain DACL.  (%lu)", GetLastError()));
    return GetLastError();
  }

  PSID sidBuiltinUsers;
  DWORD SIDSize = SECURITY_MAX_SID_SIZE;
  sidBuiltinUsers = LocalAlloc(LMEM_FIXED, SIDSize);
  if (!sidBuiltinUsers) {
    LOG_WARN(("Could not allocate SID memory.  (%lu)", GetLastError()));
    return GetLastError();
  }
  UniqueSidPtr uniqueSidBuiltinUsers(sidBuiltinUsers);

  if (!CreateWellKnownSid(WinBuiltinUsersSid, nullptr, sidBuiltinUsers,
                          &SIDSize)) {
    DWORD lastError = GetLastError();
    LOG_WARN(("Could not create BI\\Users SID.  (%lu)", lastError));
    return lastError;
  }

  PSID sidInteractive;
  SIDSize = SECURITY_MAX_SID_SIZE;
  sidInteractive = LocalAlloc(LMEM_FIXED, SIDSize);
  if (!sidInteractive) {
    LOG_WARN(("Could not allocate SID memory.  (%lu)", GetLastError()));
    return GetLastError();
  }
  UniqueSidPtr uniqueSidInteractive(sidInteractive);

  if (!CreateWellKnownSid(WinInteractiveSid, nullptr, sidInteractive,
                          &SIDSize)) {
    DWORD lastError = GetLastError();
    LOG_WARN(("Could not create Interactive SID.  (%lu)", lastError));
    return lastError;
  }

  const size_t eaCount = 2;
  EXPLICIT_ACCESS ea[eaCount];
  ZeroMemory(ea, sizeof(ea));
  ea[0].grfAccessMode = REVOKE_ACCESS;
  ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
  ea[0].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
  ea[0].Trustee.ptstrName = static_cast<LPWSTR>(sidBuiltinUsers);
  ea[1].grfAccessPermissions = SERVICE_START | SERVICE_STOP | GENERIC_READ;
  ea[1].grfAccessMode = SET_ACCESS;
  ea[1].grfInheritance = NO_INHERITANCE;
  ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
  ea[1].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
  ea[1].Trustee.ptstrName = static_cast<LPWSTR>(sidInteractive);

  DWORD lastError = SetEntriesInAclW(eaCount, ea, pacl, &pNewAcl);
  if (ERROR_SUCCESS != lastError) {
    LOG_WARN(("Could not set entries in ACL.  (%lu)", lastError));
    return lastError;
  }

  // Initialize a new security descriptor.
  SECURITY_DESCRIPTOR sd;
  if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
    LOG_WARN(
        ("Could not initialize security descriptor.  (%lu)", GetLastError()));
    return GetLastError();
  }

  // Set the new DACL in the security descriptor.
  if (!SetSecurityDescriptorDacl(&sd, TRUE, pNewAcl, FALSE)) {
    LOG_WARN(
        ("Could not set security descriptor DACL.  (%lu)", GetLastError()));
    return GetLastError();
  }

  // Set the new security descriptor for the service object.
  if (!SetServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, &sd)) {
    LOG_WARN(("Could not set object security.  (%lu)", GetLastError()));
    return GetLastError();
  }

  // Woohoo, raise the roof
  LOG(("User access was set successfully on the service."));
  return ERROR_SUCCESS;
}
