Bug 1453613 Part 2 - Add a full installer telemetry ping. r=agashlin
authorMatt Howell <mhowell@mozilla.com>
Mon, 15 Oct 2018 11:21:25 -0700
changeset 491143 32d8d8c0d44e2030e20578daaba75fe0a69d547a
parent 491142 279926d68bfc9008f082bb001ad4bc65d2f15363
child 491144 d33100811e81d7a87c080475622e3cb54f85617b
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersagashlin
bugs1453613
milestone65.0a1
Bug 1453613 Part 2 - Add a full installer telemetry ping. r=agashlin
browser/installer/windows/nsis/defines.nsi.in
browser/installer/windows/nsis/installer.nsi
browser/installer/windows/nsis/stub.nsi
toolkit/mozapps/installer/windows/nsis/common.nsh
--- a/browser/installer/windows/nsis/defines.nsi.in
+++ b/browser/installer/windows/nsis/defines.nsi.in
@@ -134,8 +134,14 @@ VIAddVersionKey "ProductVersion"  "${App
 !define PROFILE_CLEANUP_LABEL_TOP_DU 39u
 !define NOW_INSTALLING_TOP_DU 70u
 !define INSTALL_BLURB_TOP_DU 137u
 !define INSTALL_FOOTER_TOP_DU -48u
 !define INSTALL_FOOTER_WIDTH_DU 250u
 !define PROGRESS_BAR_TOP_DU 112u
 !define APPNAME_BMP_EDGE_DU 19u
 !define APPNAME_BMP_TOP_DU 12u
+
+# Constants for parts of the telemetry submission URL
+!define TELEMETRY_BASE_URL https://incoming.telemetry.mozilla.org/submit
+!define TELEMETRY_NAMESPACE firefox-installer
+!define TELEMETRY_INSTALL_PING_VERSION 1
+!define TELEMETRY_INSTALL_PING_DOCTYPE install
--- a/browser/installer/windows/nsis/installer.nsi
+++ b/browser/installer/windows/nsis/installer.nsi
@@ -1,16 +1,17 @@
 # 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/.
 
 # Required Plugins:
 # AppAssocReg    http://nsis.sourceforge.net/Application_Association_Registration_plug-in
 # ApplicationID  http://nsis.sourceforge.net/ApplicationID_plug-in
 # CityHash       http://dxr.mozilla.org/mozilla-central/source/other-licenses/nsis/Contrib/CityHash
+# nsJSON         http://nsis.sourceforge.net/NsJSON_plug-in
 # ShellLink      http://nsis.sourceforge.net/ShellLink_plug-in
 # UAC            http://nsis.sourceforge.net/UAC_plug-in
 # ServicesHelper Mozilla specific plugin that is located in /other-licenses/nsis
 
 ; Set verbosity to 3 (e.g. no script) to lessen the noise in the build logs
 !verbose 3
 
 ; 7-Zip provides better compression than the lzma from NSIS so we add the files
@@ -34,16 +35,28 @@ Var AddTaskbarSC
 Var AddQuickLaunchSC
 Var AddDesktopSC
 Var InstallMaintenanceService
 Var InstallOptionalExtensions
 Var ExtensionRecommender
 Var PageName
 Var PreventRebootRequired
 
+; Telemetry ping fields
+Var SetAsDefault
+Var HadOldInstall
+Var DefaultInstDir
+Var IntroPhaseStart
+Var OptionsPhaseStart
+Var InstallPhaseStart
+Var FinishPhaseStart
+Var FinishPhaseEnd
+Var InstallResult
+Var LaunchedNewApp
+
 ; By defining NO_STARTMENU_DIR an installer that doesn't provide an option for
 ; an application's Start Menu PROGRAMS directory and doesn't define the
 ; StartMenuDir variable can use the common InstallOnInitCommon macro.
 !define NO_STARTMENU_DIR
 
 ; Attempt to elevate Standard Users in addition to users that
 ; are a member of the Administrators group.
 !define NONADMIN_ELEVATE
@@ -79,16 +92,17 @@ VIAddVersionKey "OriginalFilename" "setu
 !insertmacro _LoggingCommon
 
 !insertmacro AddDisabledDDEHandlerValues
 !insertmacro ChangeMUIHeaderImage
 !insertmacro CheckForFilesInUse
 !insertmacro CleanUpdateDirectories
 !insertmacro CopyFilesFromDir
 !insertmacro CreateRegKey
+!insertmacro GetFirstInstallPath
 !insertmacro GetLongPath
 !insertmacro GetPathFromString
 !insertmacro GetParent
 !insertmacro InitHashAppModelId
 !insertmacro IsHandlerForInstallDir
 !insertmacro IsPinnedToTaskBar
 !insertmacro IsUserAdmin
 !insertmacro LogDesktopShortcut
@@ -183,26 +197,30 @@ Page custom preSummary leaveSummary
 !insertmacro MUI_PAGE_INSTFILES
 
 ; Finish Page
 !define MUI_FINISHPAGE_TITLE_3LINES
 !define MUI_FINISHPAGE_RUN
 !define MUI_FINISHPAGE_RUN_FUNCTION LaunchApp
 !define MUI_FINISHPAGE_RUN_TEXT $(LAUNCH_TEXT)
 !define MUI_PAGE_CUSTOMFUNCTION_PRE preFinish
+!define MUI_PAGE_CUSTOMFUNCTION_LEAVE postFinish
 !insertmacro MUI_PAGE_FINISH
 
 ; Use the default dialog for IDD_VERIFY for a simple Banner
 ChangeUI IDD_VERIFY "${NSISDIR}\Contrib\UIs\default.exe"
 
 ################################################################################
 # Install Sections
 
 ; Cleanup operations to perform at the start of the installation.
 Section "-InstallStartCleanup"
+  System::Call "kernel32::GetTickCount()l .s"
+  Pop $InstallPhaseStart
+
   SetDetailsPrint both
   DetailPrint $(STATUS_CLEANUP)
   SetDetailsPrint none
 
   SetOutPath "$INSTDIR"
   ${StartInstallLog} "${BrandFullName}" "${AB_CD}" "${AppVersion}" "${GREVersion}"
 
   StrCpy $R9 "true"
@@ -658,17 +676,17 @@ Section "-InstallEndCleanup"
   SetDetailsPrint both
   DetailPrint "$(STATUS_CLEANUP)"
   SetDetailsPrint none
 
   ${Unless} ${Silent}
     ClearErrors
     ${MUI_INSTALLOPTIONS_READ} $0 "summary.ini" "Field 4" "State"
     ${If} "$0" == "1"
-      ; NB: this code is duplicated in stub.nsi. Please keep in sync.
+      StrCpy $SetAsDefault true
       ; For data migration in the app, we want to know what the default browser
       ; value was before we changed it. To do so, we read it here and store it
       ; in our own registry key.
       StrCpy $0 ""
       AppAssocReg::QueryCurrentDefault "http" "protocol" "effective"
       Pop $1
       ; If the method hasn't failed, $1 will contain the progid. Check:
       ${If} "$1" != "method failed"
@@ -693,16 +711,17 @@ Section "-InstallEndCleanup"
       ${GetOptions} "$0" "/UAC:" $0
       ${If} ${Errors}
         Call SetAsDefaultAppUserHKCU
       ${Else}
         GetFunctionAddress $0 SetAsDefaultAppUserHKCU
         UAC::ExecCodeSegment $0
       ${EndIf}
     ${ElseIfNot} ${Errors}
+      StrCpy $SetAsDefault false
       ${LogHeader} "Writing default-browser opt-out"
       ClearErrors
       WriteRegStr HKCU "Software\Mozilla\Firefox" "DefaultBrowserOptOut" "True"
       ${If} ${Errors}
         ${LogMsg} "Error writing default-browser opt-out"
       ${EndIf}
     ${EndIf}
   ${EndUnless}
@@ -751,16 +770,24 @@ Section "-InstallEndCleanup"
         FileWrite $0 "Will be deleted on restart"
         Rename /REBOOTOK "$INSTDIR\${FileMainEXE}.moz-upgrade" "$INSTDIR\${FileMainEXE}"
         FileClose $0
         Delete "$INSTDIR\${FileMainEXE}"
         Rename "$INSTDIR\helper.exe" "$INSTDIR\${FileMainEXE}"
       ${EndUnless}
     ${EndIf}
   ${EndIf}
+
+  StrCpy $InstallResult "success"
+
+  ; When we're using the GUI, .onGUIEnd sends the ping, but of course that isn't
+  ; invoked when we're running silently.
+  ${If} ${Silent}
+    Call SendPing
+  ${EndIf}
 SectionEnd
 
 ################################################################################
 # Install Abort Survey Functions
 
 Function CustomAbort
   ${If} "${AB_CD}" == "en-US"
   ${AndIf} "$PageName" != ""
@@ -899,25 +926,191 @@ Function LaunchApp
   ${GetParameters} $0
   ${GetOptions} "$0" "/UAC:" $1
   ${If} ${Errors}
     ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\""
   ${Else}
     GetFunctionAddress $0 LaunchAppFromElevatedProcess
     UAC::ExecCodeSegment $0
   ${EndIf}
+
+  StrCpy $LaunchedNewApp true
 FunctionEnd
 
 Function LaunchAppFromElevatedProcess
   ; Set our current working directory to the application's install directory
   ; otherwise the 7-Zip temp directory will be in use and won't be deleted.
   SetOutPath "$INSTDIR"
   ${ExecAndWaitForInputIdle} "$\"$INSTDIR\${FileMainEXE}$\""
 FunctionEnd
 
+Function SendPing
+  ${GetParameters} $0
+  ${GetOptions} $0 "/LaunchedFromStub" $0
+  ${IfNot} ${Errors}
+    Return
+  ${EndIf}
+
+  ; Create a GUID to use as the unique document ID.
+  System::Call "rpcrt4::UuidCreate(g . r0)i"
+  ; StringFromGUID2 (which is what System::Call uses internally to stringify
+  ; GUIDs) includes braces in its output, and we don't want those.
+  StrCpy $0 $0 -1 1
+
+  ; Configure the HTTP request for the ping
+  nsJSON::Set /tree ping /value "{}"
+  nsJSON::Set /tree ping "Url" /value \
+    '"${TELEMETRY_BASE_URL}/${TELEMETRY_NAMESPACE}/${TELEMETRY_INSTALL_PING_DOCTYPE}/${TELEMETRY_INSTALL_PING_VERSION}/$0"'
+  nsJSON::Set /tree ping "Verb" /value '"POST"'
+  nsJSON::Set /tree ping "DataType" /value '"JSON"'
+  nsJSON::Set /tree ping "AccessType" /value '"PreConfig"'
+
+  ; Fill in the ping payload
+  nsJSON::Set /tree ping "Data" /value "{}"
+  nsJSON::Set /tree ping "Data" "installer_type" /value '"full"'
+  nsJSON::Set /tree ping "Data" "installer_version" /value '"${AppVersion}"'
+  nsJSON::Set /tree ping "Data" "build_channel" /value '"${Channel}"'
+  nsJSON::Set /tree ping "Data" "update_channel" /value '"${UpdateChannel}"'
+  nsJSON::Set /tree ping "Data" "locale" /value '"${AB_CD}"'
+
+  ReadINIStr $0 "$INSTDIR\application.ini" "App" "Version"
+  nsJSON::Set /tree ping "Data" "version" /value '"$0"'
+  ReadINIStr $0 "$INSTDIR\application.ini" "App" "BuildID"
+  nsJSON::Set /tree ping "Data" "build_id" /value '"$0"'
+
+  ${GetParameters} $0
+  ${GetOptions} $0 "/LaunchedFromMSI" $0
+  ${IfNot} ${Errors}
+    nsJSON::Set /tree ping "Data" "from_msi" /value true
+  ${EndIf}
+
+  !ifdef HAVE_64BIT_BUILD
+    nsJSON::Set /tree ping "Data" "64bit_build" /value true
+  !else
+    nsJSON::Set /tree ping "Data" "64bit_build" /value false
+  !endif
+
+  ${If} ${RunningX64}
+    nsJSON::Set /tree ping "Data" "64bit_os" /value true
+  ${Else}
+    nsJSON::Set /tree ping "Data" "64bit_os" /value false
+  ${EndIf}
+
+  ; Though these values are sometimes incorrect due to bug 444664 it happens
+  ; so rarely it isn't worth working around it by reading the registry values.
+  ${WinVerGetMajor} $0
+  ${WinVerGetMinor} $1
+  ${WinVerGetBuild} $2
+  nsJSON::Set /tree ping "Data" "os_version" /value '"$0.$1.$2"'
+  ${If} ${IsServerOS}
+    nsJSON::Set /tree ping "Data" "server_os" /value true
+  ${Else}
+    nsJSON::Set /tree ping "Data" "server_os" /value false
+  ${EndIf}
+
+  ClearErrors
+  WriteRegStr HKLM "Software\Mozilla" "${BrandShortName}InstallerTest" \
+                   "Write Test"
+  ${If} ${Errors}
+    nsJSON::Set /tree ping "Data" "admin_user" /value false
+  ${Else}
+    DeleteRegValue HKLM "Software\Mozilla" "${BrandShortName}InstallerTest"
+    nsJSON::Set /tree ping "Data" "admin_user" /value true
+  ${EndIf}
+
+  ${If} $DefaultInstDir == $INSTDIR
+    nsJSON::Set /tree ping "Data" "default_path" /value true
+  ${Else}
+    nsJSON::Set /tree ping "Data" "default_path" /value false
+  ${EndIf}
+
+  nsJSON::Set /tree ping "Data" "set_default" /value "$SetAsDefault"
+
+  nsJSON::Set /tree ping "Data" "new_default" /value false
+  nsJSON::Set /tree ping "Data" "old_default" /value false
+
+  AppAssocReg::QueryCurrentDefault "http" "protocol" "effective"
+  Pop $0
+  ReadRegStr $0 HKCR "$0\shell\open\command" ""
+  ${If} $0 != ""
+    ${GetPathFromString} "$0" $0
+    ${GetParent} "$0" $1
+    ${GetLongPath} "$1" $1
+    ${If} $1 == $INSTDIR
+      nsJSON::Set /tree ping "Data" "new_default" /value true
+    ${Else}
+      StrCpy $0 "$0" "" -11 # 11 == length of "firefox.exe"
+      ${If} "$0" == "${FileMainEXE}"
+        nsJSON::Set /tree ping "Data" "old_default" /value true
+      ${EndIf}
+    ${EndIf}
+  ${EndIf}
+
+  nsJSON::Set /tree ping "Data" "had_old_install" /value "$HadOldInstall"
+
+  ${If} ${Silent}
+    ; In silent mode, only the install phase is executed, and the GUI events
+    ; that initialize most of the phase times are never called; only
+    ; $InstallPhaseStart and $FinishPhaseStart have usable values.
+    ${GetSecondsElapsed} $InstallPhaseStart $FinishPhaseStart $0
+
+    nsJSON::Set /tree ping "Data" "intro_time" /value 0
+    nsJSON::Set /tree ping "Data" "options_time" /value 0
+    nsJSON::Set /tree ping "Data" "install_time" /value "$0"
+    nsJSON::Set /tree ping "Data" "finish_time" /value 0
+  ${Else}
+    ; In GUI mode, all we can be certain of is that the intro phase has started;
+    ; the user could have canceled at any time and phases after that won't
+    ; have run at all. So we have to be prepared for anything after
+    ; $IntroPhaseStart to be uninitialized. For anything that isn't filled in
+    ; yet we'll use the current tick count. That means that any phases that
+    ; weren't entered at all will get 0 for their times because the start and
+    ; end tick counts will be the same.
+    System::Call "kernel32::GetTickCount()l .s"
+    Pop $0
+
+    ${If} $OptionsPhaseStart == 0
+      StrCpy $OptionsPhaseStart $0
+    ${EndIf}
+    ${GetSecondsElapsed} $IntroPhaseStart $OptionsPhaseStart $1
+    nsJSON::Set /tree ping "Data" "intro_time" /value "$1"
+
+    ${If} $InstallPhaseStart == 0
+      StrCpy $InstallPhaseStart $0
+    ${EndIf}
+    ${GetSecondsElapsed} $OptionsPhaseStart $InstallPhaseStart $1
+    nsJSON::Set /tree ping "Data" "options_time" /value "$1"
+
+    ${If} $FinishPhaseStart == 0
+      StrCpy $FinishPhaseStart $0
+    ${EndIf}
+    ${GetSecondsElapsed} $InstallPhaseStart $FinishPhaseStart $1
+    nsJSON::Set /tree ping "Data" "install_time" /value "$1"
+
+    ${If} $FinishPhaseEnd == 0
+      StrCpy $FinishPhaseEnd $0
+    ${EndIf}
+    ${GetSecondsElapsed} $FinishPhaseStart $FinishPhaseEnd $1
+    nsJSON::Set /tree ping "Data" "finish_time" /value "$1"
+  ${EndIf}
+
+  nsJSON::Set /tree ping "Data" "new_launched" /value "$LaunchedNewApp"
+
+  nsJSON::Set /tree ping "Data" "succeeded" /value false
+  ${If} $InstallResult == "cancel"
+    nsJSON::Set /tree ping "Data" "user_cancelled" /value true
+  ${ElseIf} $InstallResult == "success"
+    nsJSON::Set /tree ping "Data" "succeeded" /value true
+  ${EndIf}
+
+  ; Send the ping request. This call will block until a response is received,
+  ; but we shouldn't have any windows still open, so we won't jank anything.
+  nsJSON::Set /http ping
+FunctionEnd
+
 ################################################################################
 # Language
 
 !insertmacro MOZ_MUI_LANGUAGE 'baseLocale'
 !verbose push
 !verbose 3
 !include "overrideLocale.nsh"
 !include "customLocale.nsh"
@@ -934,19 +1127,25 @@ BrandingText " "
 # Page pre, show, and leave functions
 
 Function preWelcome
   StrCpy $PageName "Welcome"
   ${If} ${FileExists} "$EXEDIR\core\distribution\modern-wizard.bmp"
     Delete "$PLUGINSDIR\modern-wizard.bmp"
     CopyFiles /SILENT "$EXEDIR\core\distribution\modern-wizard.bmp" "$PLUGINSDIR\modern-wizard.bmp"
   ${EndIf}
+
+  System::Call "kernel32::GetTickCount()l .s"
+  Pop $IntroPhaseStart
 FunctionEnd
 
 Function preOptions
+  System::Call "kernel32::GetTickCount()l .s"
+  Pop $OptionsPhaseStart
+
   StrCpy $PageName "Options"
   ${If} ${FileExists} "$EXEDIR\core\distribution\modern-header.bmp"
   ${AndIf} $hHeaderBitmap == ""
     Delete "$PLUGINSDIR\modern-header.bmp"
     CopyFiles /SILENT "$EXEDIR\core\distribution\modern-header.bmp" "$PLUGINSDIR\modern-header.bmp"
     ${ChangeMUIHeaderImage} "$PLUGINSDIR\modern-header.bmp"
   ${EndIf}
   !insertmacro MUI_HEADER_TEXT "$(OPTIONS_PAGE_TITLE)" "$(OPTIONS_PAGE_SUBTITLE)"
@@ -970,16 +1169,18 @@ Function leaveOptions
   ${If} $InstallType == ${INSTALLTYPE_BASIC}
     Call CheckExistingInstall
   ${EndIf}
 FunctionEnd
 
 Function preDirectory
   StrCpy $PageName "Directory"
   ${PreDirectoryCommon}
+
+  StrCpy $DefaultInstDir $INSTDIR
 FunctionEnd
 
 Function leaveDirectory
   ${If} $InstallType == ${INSTALLTYPE_BASIC}
     Call CheckExistingInstall
   ${EndIf}
   ${LeaveDirectoryCommon} "$(WARN_DISK_SPACE)" "$(WARN_WRITE_ACCESS)"
 FunctionEnd
@@ -1283,29 +1484,49 @@ Function leaveSummary
   ${If} ${Errors}
     ${ManualCloseAppPrompt} "${WindowClass}" "$(WARN_MANUALLY_CLOSE_APP_INSTALL)"
   ${EndIf}
 FunctionEnd
 
 ; When we add an optional action to the finish page the cancel button is
 ; enabled. This disables it and leaves the finish button as the only choice.
 Function preFinish
+  System::Call "kernel32::GetTickCount()l .s"
+  Pop $FinishPhaseStart
+
   StrCpy $PageName ""
   ${EndInstallLog} "${BrandFullName}"
   !insertmacro MUI_INSTALLOPTIONS_WRITE "ioSpecial.ini" "settings" "cancelenabled" "0"
 FunctionEnd
 
+Function postFinish
+  System::Call "kernel32::GetTickCount()l .s"
+  Pop $FinishPhaseEnd
+FunctionEnd
+
 ################################################################################
 # Initialization Functions
 
 Function .onInit
   ; Remove the current exe directory from the search order.
   ; This only effects LoadLibrary calls and not implicitly loaded DLLs.
   System::Call 'kernel32::SetDllDirectoryW(w "")'
 
+  ; Initialize the variables used for telemetry
+  StrCpy $SetAsDefault true
+  StrCpy $HadOldInstall false
+  StrCpy $DefaultInstDir $INSTDIR
+  StrCpy $IntroPhaseStart 0
+  StrCpy $OptionsPhaseStart 0
+  StrCpy $InstallPhaseStart 0
+  StrCpy $FinishPhaseStart 0
+  StrCpy $FinishPhaseEnd 0
+  StrCpy $InstallResult "cancel"
+  StrCpy $LaunchedNewApp false
+
   StrCpy $PageName ""
   StrCpy $LANGUAGE 0
   ${SetBrandNameVars} "$EXEDIR\core\distribution\setup.ini"
 
   ; Don't install on systems that don't support SSE2. The parameter value of
   ; 10 is for PF_XMMI64_INSTRUCTIONS_AVAILABLE which will check whether the
   ; SSE2 instruction set is available. Result returned in $R7.
   System::Call "kernel32::IsProcessorFeaturePresent(i 10)i .R7"
@@ -1333,16 +1554,30 @@ Function .onInit
   ${Unless} ${RunningX64}
     MessageBox MB_OKCANCEL|MB_ICONSTOP "$(WARN_MIN_SUPPORTED_OSVER_MSG)" IDCANCEL +2
     ExecShell "open" "${URLSystemRequirements}"
     Quit
   ${EndUnless}
   SetRegView 64
 !endif
 
+  SetShellVarContext all
+  ${GetFirstInstallPath} "Software\Mozilla\${BrandFullNameInternal}" $0
+  ${If} "$0" == "false"
+    SetShellVarContext current
+    ${GetFirstInstallPath} "Software\Mozilla\${BrandFullNameInternal}" $0
+    ${If} "$0" == "false"
+      StrCpy $HadOldInstall false
+    ${Else}
+      StrCpy $HadOldInstall true
+    ${EndIf}
+  ${Else}
+    StrCpy $HadOldInstall true
+  ${EndIf}
+
   ${InstallOnInitCommon} "$(WARN_MIN_SUPPORTED_OSVER_CPU_MSG)"
 
   !insertmacro InitInstallOptionsFile "options.ini"
   !insertmacro InitInstallOptionsFile "shortcuts.ini"
   !insertmacro InitInstallOptionsFile "components.ini"
   !insertmacro InitInstallOptionsFile "extensions.ini"
   !insertmacro InitInstallOptionsFile "summary.ini"
 
@@ -1522,9 +1757,10 @@ Function .onInit
 
   ; Initialize $hHeaderBitmap to prevent redundant changing of the bitmap if
   ; the user clicks the back button
   StrCpy $hHeaderBitmap ""
 FunctionEnd
 
 Function .onGUIEnd
   ${OnEndCommon}
+  Call SendPing
 FunctionEnd
--- a/browser/installer/windows/nsis/stub.nsi
+++ b/browser/installer/windows/nsis/stub.nsi
@@ -1213,17 +1213,17 @@ Function LaunchFullInstaller
   ; since it being present will require an OS restart for the full
   ; installer.
   Delete "$INSTDIR\${FileMainEXE}.moz-upgrade"
   Delete "$INSTDIR\${FileMainEXE}.moz-delete"
 
   System::Call "kernel32::GetTickCount()l .s"
   Pop $EndPreInstallPhaseTickCount
 
-  Exec "$\"$PLUGINSDIR\download.exe$\" /INI=$PLUGINSDIR\${CONFIG_INI}"
+  Exec "$\"$PLUGINSDIR\download.exe$\" /LaunchedFromStub /INI=$PLUGINSDIR\${CONFIG_INI}"
   ${NSD_CreateTimer} CheckInstall ${InstallIntervalMS}
 FunctionEnd
 
 Function SendPing
   ${NSD_KillTimer} NextBlurb
   ${NSD_KillTimer} ClearBlurb
   HideWindow
 
--- a/toolkit/mozapps/installer/windows/nsis/common.nsh
+++ b/toolkit/mozapps/installer/windows/nsis/common.nsh
@@ -1707,16 +1707,134 @@
     !insertmacro GetSingleInstallPath
 
     !undef _MOZFUNC_UN
     !define _MOZFUNC_UN
     !verbose pop
   !endif
 !macroend
 
+/**
+ * Find the first existing installation for the application.
+ * This is similar to GetSingleInstallPath, except that it always returns the
+ * first path it finds, instead of an error when more than one path exists.
+ *
+ * The shell context and the registry view should already have been set.
+ *
+ * @param   _KEY
+ *          The registry subkey (typically Software\Mozilla\App Name).
+ * @return  _RESULT
+ *          path to the install directory of the first location found, or
+ *          the string "false" if no existing installation was found.
+ *
+ * $R5 = counter for the loop's EnumRegKey
+ * $R6 = return value from EnumRegKey
+ * $R7 = return value from ReadRegStr
+ * $R8 = storage for _KEY
+ * $R9 = _KEY and _RESULT
+ */
+!macro GetFirstInstallPath
+  !ifndef ${_MOZFUNC_UN}GetFirstInstallPath
+    !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+    !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+    !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+    !insertmacro ${_MOZFUNC_UN_TMP}RemoveQuotesFromPath
+    !undef _MOZFUNC_UN
+    !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+    !undef _MOZFUNC_UN_TMP
+
+    !verbose push
+    !verbose ${_MOZFUNC_VERBOSE}
+    !define ${_MOZFUNC_UN}GetFirstInstallPath "!insertmacro ${_MOZFUNC_UN}__GetFirstInstallPathCall"
+
+    Function ${_MOZFUNC_UN}__GetFirstInstallPath
+      Exch $R9
+      Push $R8
+      Push $R7
+      Push $R6
+      Push $R5
+
+      StrCpy $R8 $R9
+      StrCpy $R9 "false"
+      StrCpy $R5 0
+
+      ${Do}
+        ClearErrors
+        EnumRegKey $R6 SHCTX $R8 $R5
+        ${If} ${Errors}
+        ${OrIf} $R6 == ""
+          ${Break}
+        ${EndIf}
+
+        IntOp $R5 $R5 + 1
+
+        ReadRegStr $R7 SHCTX "$R8\$R6\Main" "PathToExe"
+        ${If} ${Errors}
+          ${Continue}
+        ${EndIf}
+
+        ${${_MOZFUNC_UN}RemoveQuotesFromPath} "$R7" $R7
+        GetFullPathName $R7 "$R7"
+        ${If} ${Errors}
+          ${Continue}
+        ${EndIf}
+
+        StrCpy $R9 "$R7"
+        ${Break}
+      ${Loop}
+
+      ${If} $R9 != "false"
+        ${${_MOZFUNC_UN}GetLongPath} "$R9" $R9
+        ${${_MOZFUNC_UN}GetParent} "$R9" $R9
+      ${EndIf}
+
+      Pop $R5
+      Pop $R6
+      Pop $R7
+      Pop $R8
+      Exch $R9
+    FunctionEnd
+
+    !verbose pop
+  !endif
+!macroend
+
+!macro __GetFirstInstallPathCall _KEY _RESULT
+  !verbose push
+  !verbose ${_MOZFUNC_VERBOSE}
+  Push "${_KEY}"
+  Call __GetFirstInstallPath
+  Pop ${_RESULT}
+  !verbose pop
+!macroend
+
+!macro un.__GetFirstInstallPathCall _KEY _RESULT
+  !verbose push
+  !verbose ${_MOZFUNC_VERBOSE}
+  Push "${_KEY}"
+  Call un.__GetFirstInstallPath
+  Pop ${_RESULT}
+  !verbose pop
+!macroend
+
+!macro un.__GetFirstInstallPath
+  !ifndef un.__GetFirstInstallPath
+    !verbose push
+    !verbose ${_MOZFUNC_VERBOSE}
+    !undef _MOZFUNC_UN
+    !define _MOZFUNC_UN "un."
+
+    !insertmacro __GetFirstInstallPath
+
+    !undef _MOZFUNC_UN
+    !define _MOZFUNC_UN
+    !verbose pop
+  !endif
+!macroend
+
 
 ################################################################################
 # Macros for working with the file system
 
 /**
  * Attempts to delete a file if it exists. This will fail if the file is in use.
  *
  * @param   _FILE