зеркало из https://github.com/mozilla/gecko-dev.git
merge fx-team to mozilla-central a=merge
This commit is contained in:
Коммит
b4d7358820
|
@ -16,6 +16,7 @@
|
|||
|
||||
!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-aurora-latest"
|
||||
!define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=aurora&installer_lang=${AB_CD}"
|
||||
!define URLSystemRequirements "https://www.mozilla.org/firefox/system-requirements/"
|
||||
!define Channel "aurora"
|
||||
|
||||
# The installer's certificate name and issuer expected by the stub installer
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-nightly-latest"
|
||||
!define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=nightly&installer_lang=${AB_CD}"
|
||||
!define URLSystemRequirements "https://www.mozilla.org/firefox/system-requirements/"
|
||||
!define Channel "nightly"
|
||||
|
||||
# The installer's certificate name and issuer expected by the stub installer
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
!define OFFICIAL
|
||||
!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-latest"
|
||||
!define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=release&installer_lang=${AB_CD}"
|
||||
!define URLSystemRequirements "https://www.mozilla.org/firefox/system-requirements/"
|
||||
!define Channel "release"
|
||||
|
||||
# The installer's certificate name and issuer expected by the stub installer
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-latest"
|
||||
!define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=release&installer_lang=${AB_CD}"
|
||||
!define URLSystemRequirements "https://www.mozilla.org/firefox/system-requirements/"
|
||||
!define Channel "unofficial"
|
||||
|
||||
# The installer's certificate name and issuer expected by the stub installer
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
!define MinSupportedVer "Microsoft Windows XP SP2"
|
||||
#endif
|
||||
|
||||
!define MinSupportedCPU "SSE2"
|
||||
|
||||
#ifdef MOZ_MAINTENANCE_SERVICE
|
||||
!define MOZ_MAINTENANCE_SERVICE
|
||||
#endif
|
||||
|
|
|
@ -1108,7 +1108,71 @@ Function .onInit
|
|||
StrCpy $LANGUAGE 0
|
||||
${SetBrandNameVars} "$EXEDIR\core\distribution\setup.ini"
|
||||
|
||||
${InstallOnInitCommon} "$(WARN_MIN_SUPPORTED_OS_MSG)"
|
||||
; 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.
|
||||
System::Call "kernel32::IsProcessorFeaturePresent(i 10)i .R7"
|
||||
|
||||
!ifdef HAVE_64BIT_BUILD
|
||||
; Restrict x64 builds from being installed on x86 and pre Win7
|
||||
${Unless} ${RunningX64}
|
||||
${OrUnless} ${AtLeastWin7}
|
||||
${If} "$R7" == "0"
|
||||
strCpy $R7 "$(WARN_MIN_SUPPORTED_OSVER_CPU_MSG)"
|
||||
${Else}
|
||||
strCpy $R7 "$(WARN_MIN_SUPPORTED_OSVER_MSG)"
|
||||
${EndIf}
|
||||
MessageBox MB_OKCANCEL|MB_ICONSTOP "$R7" IDCANCEL +2
|
||||
ExecShell "open" "${URLSystemRequirements}"
|
||||
Quit
|
||||
${EndUnless}
|
||||
|
||||
SetRegView 64
|
||||
!else
|
||||
StrCpy $R8 "0"
|
||||
${If} ${AtMostWin2000}
|
||||
StrCpy $R8 "1"
|
||||
${EndIf}
|
||||
|
||||
${If} ${IsWinXP}
|
||||
${AndIf} ${AtMostServicePack} 1
|
||||
StrCpy $R8 "1"
|
||||
${EndIf}
|
||||
|
||||
${If} $R8 == "1"
|
||||
; XXX-rstrong - some systems failed the AtLeastWin2000 test that we
|
||||
; used to use for an unknown reason and likely fail the AtMostWin2000
|
||||
; and possibly the IsWinXP test as well. To work around this also
|
||||
; check if the Windows NT registry Key exists and if it does if the
|
||||
; first char in CurrentVersion is equal to 3 (Windows NT 3.5 and
|
||||
; 3.5.1), 4 (Windows NT 4), or 5 (Windows 2000 and Windows XP).
|
||||
StrCpy $R8 ""
|
||||
ClearErrors
|
||||
ReadRegStr $R8 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion" "CurrentVersion"
|
||||
StrCpy $R8 "$R8" 1
|
||||
${If} ${Errors}
|
||||
${OrIf} "$R8" == "3"
|
||||
${OrIf} "$R8" == "4"
|
||||
${OrIf} "$R8" == "5"
|
||||
${If} "$R7" == "0"
|
||||
strCpy $R7 "$(WARN_MIN_SUPPORTED_OSVER_CPU_MSG)"
|
||||
${Else}
|
||||
strCpy $R7 "$(WARN_MIN_SUPPORTED_OSVER_MSG)"
|
||||
${EndIf}
|
||||
MessageBox MB_OKCANCEL|MB_ICONSTOP "$R7" IDCANCEL +2
|
||||
ExecShell "open" "${URLSystemRequirements}"
|
||||
Quit
|
||||
${EndIf}
|
||||
${EndUnless}
|
||||
!endif
|
||||
|
||||
${If} "$R7" == "0"
|
||||
MessageBox MB_OKCANCEL|MB_ICONSTOP "$(WARN_MIN_SUPPORTED_CPU_MSG)" IDCANCEL +2
|
||||
ExecShell "open" "${URLSystemRequirements}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
${InstallOnInitCommon} "$(WARN_MIN_SUPPORTED_OSVER_CPU_MSG)"
|
||||
|
||||
; The commands inside this ifndef are needed prior to NSIS 3.0a2 and can be
|
||||
; removed after we require NSIS 3.0a2 or greater.
|
||||
|
|
|
@ -316,11 +316,22 @@ Function .onInit
|
|||
; isn't supported for the stub installer.
|
||||
${SetBrandNameVars} "$PLUGINSDIR\ignored.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.
|
||||
System::Call "kernel32::IsProcessorFeaturePresent(i 10)i .R7"
|
||||
|
||||
!ifdef HAVE_64BIT_BUILD
|
||||
; Restrict x64 builds from being installed on x86 and pre Win7
|
||||
${Unless} ${RunningX64}
|
||||
${OrUnless} ${AtLeastWin7}
|
||||
MessageBox MB_OK|MB_ICONSTOP "$(WARN_MIN_SUPPORTED_OS_MSG)"
|
||||
${If} "$R7" == "0"
|
||||
strCpy $R7 "$(WARN_MIN_SUPPORTED_OSVER_CPU_MSG)"
|
||||
${Else}
|
||||
strCpy $R7 "$(WARN_MIN_SUPPORTED_OSVER_MSG)"
|
||||
${EndIf}
|
||||
MessageBox MB_OKCANCEL|MB_ICONSTOP "$R7" IDCANCEL +2
|
||||
ExecShell "open" "${URLSystemRequirements}"
|
||||
Quit
|
||||
${EndUnless}
|
||||
|
||||
|
@ -351,12 +362,24 @@ Function .onInit
|
|||
${OrIf} "$R8" == "3"
|
||||
${OrIf} "$R8" == "4"
|
||||
${OrIf} "$R8" == "5"
|
||||
MessageBox MB_OK|MB_ICONSTOP "$(WARN_MIN_SUPPORTED_OS_MSG)"
|
||||
${If} "$R7" == "0"
|
||||
strCpy $R7 "$(WARN_MIN_SUPPORTED_OSVER_CPU_MSG)"
|
||||
${Else}
|
||||
strCpy $R7 "$(WARN_MIN_SUPPORTED_OSVER_MSG)"
|
||||
${EndIf}
|
||||
MessageBox MB_OKCANCEL|MB_ICONSTOP "$R7" IDCANCEL +2
|
||||
ExecShell "open" "${URLSystemRequirements}"
|
||||
Quit
|
||||
${EndIf}
|
||||
${EndUnless}
|
||||
!endif
|
||||
|
||||
${If} "$R7" == "0"
|
||||
MessageBox MB_OKCANCEL|MB_ICONSTOP "$(WARN_MIN_SUPPORTED_CPU_MSG)" IDCANCEL +2
|
||||
ExecShell "open" "${URLSystemRequirements}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
; Require elevation if the user can elevate
|
||||
${ElevateUAC}
|
||||
|
||||
|
|
|
@ -48,7 +48,9 @@ WARN_MANUALLY_CLOSE_APP_UNINSTALL=$BrandShortName must be closed to proceed with
|
|||
WARN_MANUALLY_CLOSE_APP_LAUNCH=$BrandShortName is already running.\n\nPlease close $BrandShortName prior to launching the version you have just installed.
|
||||
WARN_WRITE_ACCESS=You don't have access to write to the installation directory.\n\nClick OK to select a different directory.
|
||||
WARN_DISK_SPACE=You don't have sufficient disk space to install to this location.\n\nClick OK to select a different location.
|
||||
WARN_MIN_SUPPORTED_OS_MSG=Sorry, $BrandShortName can't be installed. This version of $BrandShortName requires ${MinSupportedVer} or newer.
|
||||
WARN_MIN_SUPPORTED_OSVER_MSG=Sorry, $BrandShortName can't be installed. This version of $BrandShortName requires ${MinSupportedVer} or newer. Please click the OK button for additional information.
|
||||
WARN_MIN_SUPPORTED_CPU_MSG=Sorry, $BrandShortName can't be installed. This version of $BrandShortName requires a processor with ${MinSupportedCPU} support. Please click the OK button for additional information.
|
||||
WARN_MIN_SUPPORTED_OSVER_CPU_MSG=Sorry, $BrandShortName can't be installed. This version of $BrandShortName requires ${MinSupportedVer} or newer and a processor with ${MinSupportedCPU} support. Please click the OK button for additional information.
|
||||
WARN_RESTART_REQUIRED_UNINSTALL=Your computer must be restarted to complete a previous uninstall of $BrandShortName. Do you want to reboot now?
|
||||
WARN_RESTART_REQUIRED_UPGRADE=Your computer must be restarted to complete a previous upgrade of $BrandShortName. Do you want to reboot now?
|
||||
ERROR_CREATE_DIRECTORY_PREFIX=Error creating directory:
|
||||
|
|
|
@ -25,7 +25,9 @@ INSTALL_BLURB1=You're about to enjoy the very latest in speed, flexibility and s
|
|||
INSTALL_BLURB2=That's because $BrandShortName is made by a non-profit to make browsing and the Web better for you.
|
||||
INSTALL_BLURB3=You're also joining a global community of users, contributors and developers working to make the best browser in the world.
|
||||
|
||||
WARN_MIN_SUPPORTED_OS_MSG=Sorry, $BrandShortName can't be installed. This version of $BrandShortName requires ${MinSupportedVer} or newer.
|
||||
WARN_MIN_SUPPORTED_OSVER_MSG=Sorry, $BrandShortName can't be installed. This version of $BrandShortName requires ${MinSupportedVer} or newer. Please click the OK button for additional information.
|
||||
WARN_MIN_SUPPORTED_CPU_MSG=Sorry, $BrandShortName can't be installed. This version of $BrandShortName requires a processor with ${MinSupportedCPU} support. Please click the OK button for additional information.
|
||||
WARN_MIN_SUPPORTED_OSVER_CPU_MSG=Sorry, $BrandShortName can't be installed. This version of $BrandShortName requires ${MinSupportedVer} or newer and a processor with ${MinSupportedCPU} support. Please click the OK button for additional information.
|
||||
WARN_WRITE_ACCESS=You don't have access to write to the installation directory.\n\nClick OK to select a different directory.
|
||||
WARN_DISK_SPACE=You don't have sufficient disk space to install to this location.\n\nClick OK to select a different location.
|
||||
WARN_ROOT_INSTALL=Unable to install to the root of your disk.\n\nClick OK to select a different location.
|
||||
|
|
|
@ -87,12 +87,10 @@
|
|||
margin-left: 7px;
|
||||
}
|
||||
|
||||
@media (-moz-mac-lion-theme) {
|
||||
.titlebar-placeholder[type="fullscreen-button"],
|
||||
#titlebar-secondary-buttonbox {
|
||||
margin-right: 7px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
.titlebar-placeholder[type="fullscreen-button"],
|
||||
#titlebar-secondary-buttonbox {
|
||||
margin-right: 7px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
#main-window:not(:-moz-lwtheme) > #titlebar {
|
||||
|
@ -1062,31 +1060,23 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
|
|||
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-badge-stack > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1 > .toolbarbutton-menubutton-button[disabled="true"] > .toolbarbutton-icon {
|
||||
opacity: .4;
|
||||
#main-window:not([customizing]) .toolbarbutton-1 > .toolbarbutton-menubutton-button[disabled="true"] > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-menu-dropmarker,
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-menubutton-dropmarker,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > #downloads-indicator-anchor,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-icon,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-text,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-badge-stack > .toolbarbutton-icon,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-menu-dropmarker,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
@media (-moz-mac-lion-theme) {
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-badge-stack > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1 > .toolbarbutton-menubutton-button[disabled="true"] > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-menu-dropmarker,
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-menubutton-dropmarker,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > #downloads-indicator-anchor,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-icon,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-text,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-badge-stack > .toolbarbutton-icon,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-menu-dropmarker,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon,
|
||||
.toolbarbutton-1:not(:hover):-moz-window-inactive > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
#main-window:not([customizing]) .toolbarbutton-1:-moz-window-inactive[disabled="true"] > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1:-moz-window-inactive[disabled="true"] > .toolbarbutton-badge-stack > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1:-moz-window-inactive > .toolbarbutton-menubutton-button[disabled="true"] > .toolbarbutton-icon {
|
||||
opacity: .25;
|
||||
}
|
||||
#main-window:not([customizing]) .toolbarbutton-1:-moz-window-inactive[disabled="true"] > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1:-moz-window-inactive[disabled="true"] > .toolbarbutton-badge-stack > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1:-moz-window-inactive > .toolbarbutton-menubutton-button[disabled="true"] > .toolbarbutton-icon {
|
||||
opacity: .25;
|
||||
}
|
||||
|
||||
.toolbarbutton-1 > .toolbarbutton-menu-dropmarker,
|
||||
|
@ -1222,50 +1212,28 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
|
|||
}
|
||||
|
||||
#forward-button:not(:-moz-lwtheme) {
|
||||
background: linear-gradient(hsl(0,0%,99%), hsl(0,0%,67%)) padding-box;
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.73), hsla(0,0%,100%,.05) 85%);
|
||||
border: 1px solid;
|
||||
border-color: hsl(0,0%,31%) hsla(0,0%,29%,.6) hsl(0,0%,27%);
|
||||
box-shadow: inset 0 1px 0 hsla(0,0%,100%,.35),
|
||||
border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.25) hsla(0,0%,0%,.2);
|
||||
box-shadow: inset 0 1px 0 hsla(0,0%,100%,.2),
|
||||
inset 0 0 1px hsla(0,0%,100%,.1),
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
}
|
||||
|
||||
#forward-button:hover:active:not(:-moz-lwtheme) {
|
||||
background-image: linear-gradient(hsl(0,0%,74%), hsl(0,0%,61%));
|
||||
box-shadow: inset rgba(0,0,0,.3) 0 -6px 10px,
|
||||
inset #000 0 1px 3px,
|
||||
inset rgba(0,0,0,.2) 0 1px 3px,
|
||||
background-image: linear-gradient(hsla(0,0%,60%,.37), hsla(0,0%,100%,.35) 95%);
|
||||
border-color: hsla(0,0%,0%,.43) hsla(0,0%,0%,.25) hsla(0,0%,0%,.37);
|
||||
box-shadow: inset 0 1px 0 hsla(0,0%,0%,.02),
|
||||
inset 0 1px 2px hsla(0,0%,0%,.2),
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
}
|
||||
|
||||
#forward-button:-moz-window-inactive:not(:-moz-lwtheme) {
|
||||
border-color: hsl(0,0%,64%) hsl(0,0%,65%) hsl(0,0%,66%);
|
||||
background-image: linear-gradient(hsl(0,0%,99%), hsl(0,0%,82%));
|
||||
background-image: none;
|
||||
border-color: hsla(0,0%,0%,.2);
|
||||
box-shadow: inset 0 1px 0 hsla(0,0%,100%,.35);
|
||||
}
|
||||
|
||||
@media (-moz-mac-lion-theme) {
|
||||
#forward-button:not(:-moz-lwtheme) {
|
||||
background-image: linear-gradient(hsla(0,0%,100%,.73), hsla(0,0%,100%,.05) 85%);
|
||||
border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.25) hsla(0,0%,0%,.2);
|
||||
box-shadow: inset 0 1px 0 hsla(0,0%,100%,.2),
|
||||
inset 0 0 1px hsla(0,0%,100%,.1),
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
}
|
||||
|
||||
#forward-button:hover:active:not(:-moz-lwtheme) {
|
||||
background-image: linear-gradient(hsla(0,0%,60%,.37), hsla(0,0%,100%,.35) 95%);
|
||||
border-color: hsla(0,0%,0%,.43) hsla(0,0%,0%,.25) hsla(0,0%,0%,.37);
|
||||
box-shadow: inset 0 1px 0 hsla(0,0%,0%,.02),
|
||||
inset 0 1px 2px hsla(0,0%,0%,.2),
|
||||
0 1px 0 hsla(0,0%,100%,.2);
|
||||
}
|
||||
|
||||
#forward-button:-moz-window-inactive:not(:-moz-lwtheme) {
|
||||
background-image: none;
|
||||
border-color: hsla(0,0%,0%,.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (-moz-mac-yosemite-theme) {
|
||||
/* Base and hover styles */
|
||||
#forward-button:not(:-moz-lwtheme),
|
||||
|
@ -1488,11 +1456,14 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
|
|||
.searchbar-textbox {
|
||||
font: icon;
|
||||
-moz-appearance: none;
|
||||
box-shadow: 0 1px rgba(255, 255, 255, 0.2), inset 0 1px hsla(0,0%,0%,.05);
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.2),
|
||||
inset 0 0 1px hsla(0,0%,0%,.05),
|
||||
inset 0 1px 2px hsla(0,0%,0%,.1);
|
||||
margin: 0 4px;
|
||||
padding: 1px 0;
|
||||
border: 1px solid;
|
||||
border-color: #626262 #787878 #8c8c8c;
|
||||
background-image: linear-gradient(hsl(0,0%,97%), hsl(0,0%,100%));
|
||||
border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.25) hsla(0,0%,0%,.15);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
@ -1500,24 +1471,6 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
|
|||
background-color: -moz-field;
|
||||
}
|
||||
|
||||
@media (-moz-mac-lion-theme) {
|
||||
#urlbar,
|
||||
.searchbar-textbox {
|
||||
background-image: linear-gradient(hsl(0,0%,97%), hsl(0,0%,100%));
|
||||
border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.25) hsla(0,0%,0%,.15);
|
||||
box-shadow: 0 1px 0 hsla(0,0%,100%,.2),
|
||||
inset 0 0 1px hsla(0,0%,0%,.05),
|
||||
inset 0 1px 2px hsla(0,0%,0%,.1);
|
||||
}
|
||||
}
|
||||
|
||||
@media not all and (-moz-mac-lion-theme) {
|
||||
#urlbar:-moz-window-inactive,
|
||||
.searchbar-textbox:-moz-window-inactive {
|
||||
border-color: @toolbarbuttonInactiveBorderColor@;
|
||||
}
|
||||
}
|
||||
|
||||
@media (-moz-mac-yosemite-theme) {
|
||||
.searchbar-textbox,
|
||||
#urlbar {
|
||||
|
@ -2590,10 +2543,8 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
|
|||
text-shadow: @loweredShadow@;
|
||||
}
|
||||
|
||||
@media (-moz-mac-lion-theme) {
|
||||
#navigator-toolbox[inFullscreen] > #TabsToolbar {
|
||||
padding-top: var(--space-above-tabbar);
|
||||
}
|
||||
#navigator-toolbox[inFullscreen] > #TabsToolbar {
|
||||
padding-top: var(--space-above-tabbar);
|
||||
}
|
||||
|
||||
#tabbrowser-tabs {
|
||||
|
@ -3529,25 +3480,23 @@ menulist.translate-infobar-element > .menulist-dropmarker {
|
|||
}
|
||||
}
|
||||
|
||||
@media (-moz-mac-lion-theme) {
|
||||
#TabsToolbar > .private-browsing-indicator {
|
||||
transform: translateY(calc(-1 * var(--space-above-tabbar)));
|
||||
/* We offset by 38px for mask graphic, plus 4px to account for the
|
||||
* margin-left, which sums to 42px.
|
||||
*/
|
||||
margin-right: -42px;
|
||||
}
|
||||
#TabsToolbar > .private-browsing-indicator {
|
||||
transform: translateY(calc(-1 * var(--space-above-tabbar)));
|
||||
/* We offset by 38px for mask graphic, plus 4px to account for the
|
||||
* margin-left, which sums to 42px.
|
||||
*/
|
||||
margin-right: -42px;
|
||||
}
|
||||
|
||||
#main-window[privatebrowsingmode=temporary] .titlebar-placeholder[type="fullscreen-button"],
|
||||
#main-window[privatebrowsingmode=temporary] > #titlebar > #titlebar-content > #titlebar-secondary-buttonbox > #titlebar-fullscreen-button {
|
||||
margin-left: 0px;
|
||||
}
|
||||
#main-window[privatebrowsingmode=temporary] .titlebar-placeholder[type="fullscreen-button"],
|
||||
#main-window[privatebrowsingmode=temporary] > #titlebar > #titlebar-content > #titlebar-secondary-buttonbox > #titlebar-fullscreen-button {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
#main-window[privatebrowsingmode=temporary][inFullscreen] .titlebar-placeholder[type="fullscreen-button"] {
|
||||
/* Override display:none for .titlebar-placeholder in fullscreen so we can have consistent
|
||||
position and padding for the private browsing indicator. */
|
||||
display: -moz-box;
|
||||
}
|
||||
#main-window[privatebrowsingmode=temporary][inFullscreen] .titlebar-placeholder[type="fullscreen-button"] {
|
||||
/* Override display:none for .titlebar-placeholder in fullscreen so we can have consistent
|
||||
position and padding for the private browsing indicator. */
|
||||
display: -moz-box;
|
||||
}
|
||||
|
||||
#TabsToolbar > .private-browsing-indicator:-moz-locale-dir(rtl) {
|
||||
|
|
|
@ -156,10 +156,6 @@
|
|||
-moz-appearance: toolbarbutton;
|
||||
}
|
||||
|
||||
#placesToolbar > toolbarbutton[disabled="true"] > .toolbarbutton-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#placesToolbar > toolbarbutton > .toolbarbutton-icon {
|
||||
margin: 1px 4px;
|
||||
}
|
||||
|
@ -185,18 +181,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (-moz-mac-lion-theme) {
|
||||
#placesToolbar > toolbarbutton[disabled="true"] > .toolbarbutton-icon,
|
||||
#placesToolbar > toolbarbutton:not(:hover):-moz-window-inactive > .toolbarbutton-icon,
|
||||
#placesToolbar > toolbarbutton[type="menu"][disabled="true"] > .toolbarbutton-menu-dropmarker,
|
||||
#placesToolbar > toolbarbutton:not(:hover):-moz-window-inactive[type="menu"] > .toolbarbutton-menu-dropmarker {
|
||||
opacity: .5;
|
||||
}
|
||||
#placesToolbar > toolbarbutton[disabled="true"] > .toolbarbutton-icon,
|
||||
#placesToolbar > toolbarbutton:not(:hover):-moz-window-inactive > .toolbarbutton-icon,
|
||||
#placesToolbar > toolbarbutton[type="menu"][disabled="true"] > .toolbarbutton-menu-dropmarker,
|
||||
#placesToolbar > toolbarbutton:not(:hover):-moz-window-inactive[type="menu"] > .toolbarbutton-menu-dropmarker {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
#placesToolbar > toolbarbutton:-moz-window-inactive[disabled="true"] > .toolbarbutton-icon,
|
||||
#placesToolbar > toolbarbutton:-moz-window-inactive[type="menu"][disabled="true"] > .toolbarbutton-menu-dropmarker {
|
||||
opacity: .25;
|
||||
}
|
||||
#placesToolbar > toolbarbutton:-moz-window-inactive[disabled="true"] > .toolbarbutton-icon,
|
||||
#placesToolbar > toolbarbutton:-moz-window-inactive[type="menu"][disabled="true"] > .toolbarbutton-menu-dropmarker {
|
||||
opacity: .25;
|
||||
}
|
||||
|
||||
#placesToolbar > toolbarbutton > menupopup {
|
||||
|
|
|
@ -112,6 +112,7 @@ button {
|
|||
margin: 5px 4px 5px 0px;
|
||||
}
|
||||
|
||||
.service-worker-disabled .warning,
|
||||
.addons-install-error .warning {
|
||||
background-image: url(chrome://devtools/skin/images/alerticon-warning.png);
|
||||
background-size: 13px 12px;
|
||||
|
@ -122,6 +123,7 @@ button {
|
|||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
.service-worker-disabled .warning,
|
||||
.addons-install-error .warning {
|
||||
background-image: url(chrome://devtools/skin/images/alerticon-warning@2x.png);
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ module.exports = createClass({
|
|||
}, Strings.GetStringFromName("addonDebugging.label")),
|
||||
"(",
|
||||
dom.a({ href: MORE_INFO_URL, target: "_blank" },
|
||||
Strings.GetStringFromName("addonDebugging.moreInfo")),
|
||||
Strings.GetStringFromName("moreInfo")),
|
||||
")"
|
||||
),
|
||||
dom.button({
|
||||
|
|
|
@ -19,7 +19,7 @@ module.exports = createClass({
|
|||
displayName: "TargetList",
|
||||
|
||||
render() {
|
||||
let { client, debugDisabled, targetClass, targets, sort } = this.props;
|
||||
let { client, debugDisabled, error, targetClass, targets, sort } = this.props;
|
||||
if (sort) {
|
||||
targets = targets.sort(LocaleCompare);
|
||||
}
|
||||
|
@ -27,11 +27,16 @@ module.exports = createClass({
|
|||
return targetClass({ client, target, debugDisabled });
|
||||
});
|
||||
|
||||
let content = "";
|
||||
if (error) {
|
||||
content = error;
|
||||
} else if (targets.length > 0) {
|
||||
content = dom.ul({ className: "target-list" }, targets);
|
||||
} else {
|
||||
content = dom.p(null, Strings.GetStringFromName("nothing"));
|
||||
}
|
||||
|
||||
return dom.div({ id: this.props.id, className: "targets" },
|
||||
dom.h2(null, this.props.name),
|
||||
targets.length > 0 ?
|
||||
dom.ul({ className: "target-list" }, targets) :
|
||||
dom.p(null, Strings.GetStringFromName("nothing"))
|
||||
);
|
||||
dom.h2(null, this.props.name), content);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
/* 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/. */
|
||||
|
||||
/* globals window */
|
||||
"use strict";
|
||||
|
||||
loader.lazyImporter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
const { Ci } = require("chrome");
|
||||
const { createClass, createFactory, DOM: dom } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
|
@ -19,6 +22,7 @@ const Strings = Services.strings.createBundle(
|
|||
"chrome://devtools/locale/aboutdebugging.properties");
|
||||
|
||||
const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
|
||||
const MORE_INFO_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging";
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "WorkersPanel",
|
||||
|
@ -103,6 +107,21 @@ module.exports = createClass({
|
|||
let { client, id } = this.props;
|
||||
let { workers } = this.state;
|
||||
|
||||
let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
|
||||
let isPrivateBrowsingMode = PrivateBrowsingUtils.permanentPrivateBrowsing;
|
||||
let isServiceWorkerDisabled = !Services.prefs
|
||||
.getBoolPref("dom.serviceWorkers.enabled");
|
||||
let errorMsg = isWindowPrivate || isPrivateBrowsingMode ||
|
||||
isServiceWorkerDisabled ?
|
||||
dom.p({ className: "service-worker-disabled" },
|
||||
dom.div({ className: "warning" }),
|
||||
Strings.GetStringFromName("configurationIsNotCompatible"),
|
||||
" (",
|
||||
dom.a({ href: MORE_INFO_URL, target: "_blank" },
|
||||
Strings.GetStringFromName("moreInfo")),
|
||||
")"
|
||||
) : "";
|
||||
|
||||
return dom.div({
|
||||
id: id + "-panel",
|
||||
className: "panel",
|
||||
|
@ -116,6 +135,7 @@ module.exports = createClass({
|
|||
dom.div({ id: "workers", className: "inverted-icons" },
|
||||
TargetList({
|
||||
client,
|
||||
error: errorMsg,
|
||||
id: "service-workers",
|
||||
name: Strings.GetStringFromName("serviceWorkers"),
|
||||
sort: true,
|
||||
|
|
|
@ -19,6 +19,7 @@ support-files =
|
|||
[browser_addons_toggle_debug.js]
|
||||
[browser_page_not_found.js]
|
||||
[browser_service_workers.js]
|
||||
[browser_service_workers_not_compatible.js]
|
||||
[browser_service_workers_push.js]
|
||||
[browser_service_workers_start.js]
|
||||
[browser_service_workers_timeout.js]
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that Service Worker section should show warning message in
|
||||
// about:debugging if any of following conditions is met:
|
||||
// 1. service worker is disabled
|
||||
// 2. the about:debugging pannel is openned in private browsing mode
|
||||
// 3. the about:debugging pannel is openned in private content window
|
||||
|
||||
var imgClass = ".service-worker-disabled .warning";
|
||||
|
||||
add_task(function* () {
|
||||
yield new Promise(done => {
|
||||
info("disable service workers");
|
||||
let options = {"set": [
|
||||
["dom.serviceWorkers.enabled", false],
|
||||
]};
|
||||
SpecialPowers.pushPrefEnv(options, done);
|
||||
});
|
||||
|
||||
let { tab, document } = yield openAboutDebugging("workers");
|
||||
// Check that the warning img appears in the UI
|
||||
let img = document.querySelector(imgClass);
|
||||
ok(img, "warning message is rendered");
|
||||
|
||||
yield closeAboutDebugging(tab);
|
||||
});
|
||||
|
||||
add_task(function* () {
|
||||
yield new Promise(done => {
|
||||
info("set private browsing mode as default");
|
||||
let options = {"set": [
|
||||
["browser.privatebrowsing.autostart", true],
|
||||
]};
|
||||
SpecialPowers.pushPrefEnv(options, done);
|
||||
});
|
||||
|
||||
let { tab, document } = yield openAboutDebugging("workers");
|
||||
// Check that the warning img appears in the UI
|
||||
let img = document.querySelector(imgClass);
|
||||
ok(img, "warning message is rendered");
|
||||
|
||||
yield closeAboutDebugging(tab);
|
||||
});
|
||||
|
||||
add_task(function* () {
|
||||
info("Opening a new private window");
|
||||
let win = OpenBrowserWindow({private: true});
|
||||
yield waitForDelayedStartupFinished(win);
|
||||
|
||||
let { tab, document } = yield openAboutDebugging("workers", win);
|
||||
// Check that the warning img appears in the UI
|
||||
let img = document.querySelector(imgClass);
|
||||
ok(img, "warning message is rendered");
|
||||
|
||||
yield closeAboutDebugging(tab, win);
|
||||
win.close();
|
||||
});
|
|
@ -6,7 +6,8 @@
|
|||
/* exported openAboutDebugging, changeAboutDebuggingHash, closeAboutDebugging,
|
||||
installAddon, uninstallAddon, waitForMutation, assertHasTarget,
|
||||
getServiceWorkerList, getTabList, openPanel, waitForInitialAddonList,
|
||||
waitForServiceWorkerRegistered, unregisterServiceWorker */
|
||||
waitForServiceWorkerRegistered, unregisterServiceWorker,
|
||||
waitForDelayedStartupFinished */
|
||||
|
||||
"use strict";
|
||||
|
||||
|
@ -24,14 +25,14 @@ registerCleanupFunction(() => {
|
|||
DevToolsUtils.testing = false;
|
||||
});
|
||||
|
||||
function* openAboutDebugging(page) {
|
||||
function* openAboutDebugging(page, win) {
|
||||
info("opening about:debugging");
|
||||
let url = "about:debugging";
|
||||
if (page) {
|
||||
url += "#" + page;
|
||||
}
|
||||
|
||||
let tab = yield addTab(url);
|
||||
let tab = yield addTab(url, win);
|
||||
let browser = tab.linkedBrowser;
|
||||
let document = browser.contentDocument;
|
||||
|
||||
|
@ -63,9 +64,9 @@ function openPanel(document, panelId) {
|
|||
document.querySelector(".main-content"), {childList: true});
|
||||
}
|
||||
|
||||
function closeAboutDebugging(tab) {
|
||||
function closeAboutDebugging(tab, win) {
|
||||
info("Closing about:debugging");
|
||||
return removeTab(tab);
|
||||
return removeTab(tab, win);
|
||||
}
|
||||
|
||||
function addTab(url, win, backgroundTab = false) {
|
||||
|
@ -296,3 +297,20 @@ function unregisterServiceWorker(tab) {
|
|||
yield registration.unregister();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the creation of a new window, usually used with create private
|
||||
* browsing window.
|
||||
* Returns a promise that will resolve when the window is successfully created.
|
||||
* @param {window} win
|
||||
*/
|
||||
function waitForDelayedStartupFinished(win) {
|
||||
return new Promise(function (resolve) {
|
||||
Services.obs.addObserver(function observer(subject, topic) {
|
||||
if (win == subject) {
|
||||
Services.obs.removeObserver(observer, topic);
|
||||
resolve();
|
||||
}
|
||||
}, "browser-delayed-startup-finished", false);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ unregister = unregister
|
|||
addons = Add-ons
|
||||
addonDebugging.label = Enable add-on debugging
|
||||
addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
|
||||
addonDebugging.moreInfo = more info
|
||||
moreInfo = more info
|
||||
loadTemporaryAddon = Load Temporary Add-on
|
||||
extensions = Extensions
|
||||
selectAddonFromFile2 = Select Manifest File or Package (.xpi)
|
||||
|
@ -30,3 +30,4 @@ pageNotFound = Page not found
|
|||
doesNotExist = #%S does not exist!
|
||||
|
||||
nothing = Nothing yet.
|
||||
configurationIsNotCompatible = Your browser configuration is not compatible with Service Workers
|
||||
|
|
|
@ -14,12 +14,14 @@
|
|||
#if defined(XP_UNIX)
|
||||
#include "unistd.h"
|
||||
#include "dirent.h"
|
||||
#include "poll.h"
|
||||
#include "sys/stat.h"
|
||||
#if defined(ANDROID)
|
||||
#include <sys/vfs.h>
|
||||
#define statvfs statfs
|
||||
#else
|
||||
#include "sys/statvfs.h"
|
||||
#include "sys/wait.h"
|
||||
#include <spawn.h>
|
||||
#endif // defined(ANDROID)
|
||||
#endif // defined(XP_UNIX)
|
||||
|
@ -404,6 +406,9 @@ static const dom::ConstantSpec gLibcProperties[] =
|
|||
{
|
||||
// Arguments for open
|
||||
INT_CONSTANT(O_APPEND),
|
||||
#if defined(O_CLOEXEC)
|
||||
INT_CONSTANT(O_CLOEXEC),
|
||||
#endif // defined(O_CLOEXEC)
|
||||
INT_CONSTANT(O_CREAT),
|
||||
#if defined(O_DIRECTORY)
|
||||
INT_CONSTANT(O_DIRECTORY),
|
||||
|
@ -441,6 +446,10 @@ static const dom::ConstantSpec gLibcProperties[] =
|
|||
INT_CONSTANT(O_TRUNC),
|
||||
INT_CONSTANT(O_WRONLY),
|
||||
|
||||
#if defined(FD_CLOEXEC)
|
||||
INT_CONSTANT(FD_CLOEXEC),
|
||||
#endif // defined(FD_CLOEXEC)
|
||||
|
||||
#if defined(AT_EACCESS)
|
||||
INT_CONSTANT(AT_EACCESS),
|
||||
#endif //defined(AT_EACCESS)
|
||||
|
@ -482,13 +491,27 @@ static const dom::ConstantSpec gLibcProperties[] =
|
|||
INT_CONSTANT(SEEK_END),
|
||||
INT_CONSTANT(SEEK_SET),
|
||||
|
||||
// fcntl command values
|
||||
#if defined(XP_UNIX)
|
||||
// poll
|
||||
INT_CONSTANT(POLLERR),
|
||||
INT_CONSTANT(POLLHUP),
|
||||
INT_CONSTANT(POLLIN),
|
||||
INT_CONSTANT(POLLNVAL),
|
||||
INT_CONSTANT(POLLOUT),
|
||||
|
||||
// wait
|
||||
#if defined(WNOHANG)
|
||||
INT_CONSTANT(WNOHANG),
|
||||
#endif // defined(WNOHANG)
|
||||
|
||||
// fcntl command values
|
||||
INT_CONSTANT(F_GETLK),
|
||||
INT_CONSTANT(F_SETFD),
|
||||
INT_CONSTANT(F_SETFL),
|
||||
INT_CONSTANT(F_SETLK),
|
||||
INT_CONSTANT(F_SETLKW),
|
||||
|
||||
// flock type values
|
||||
// flock type values
|
||||
INT_CONSTANT(F_RDLCK),
|
||||
INT_CONSTANT(F_WRLCK),
|
||||
INT_CONSTANT(F_UNLCK),
|
||||
|
|
|
@ -4248,6 +4248,7 @@ pref("signon.storeWhenAutocompleteOff", true);
|
|||
pref("signon.ui.experimental", false);
|
||||
pref("signon.debug", false);
|
||||
pref("signon.recipes.path", "chrome://passwordmgr/content/recipes.json");
|
||||
pref("signon.schemeUpgrades", false);
|
||||
|
||||
// Satchel (Form Manager) prefs
|
||||
pref("browser.formfill.debug", false);
|
||||
|
|
|
@ -268,30 +268,6 @@ TabStore.prototype = {
|
|||
this._remoteClients[record.id] = Object.assign({}, record.cleartext, {
|
||||
lastModified: record.modified
|
||||
});
|
||||
|
||||
// We should remove the following code which sets the notifyTabState pref,
|
||||
// it doesn't seem to be used anywhere and appears broken (we divide by
|
||||
// 1000 but it is already seconds!). See bug 1272806.
|
||||
|
||||
// Lose some precision, but that's good enough (seconds).
|
||||
let roundModify = Math.floor(record.modified / 1000);
|
||||
let notifyState = Svc.Prefs.get("notifyTabState");
|
||||
|
||||
// If there's no existing pref, save this first modified time.
|
||||
if (notifyState == null) {
|
||||
Svc.Prefs.set("notifyTabState", roundModify);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't change notifyState if it's already 0 (don't notify).
|
||||
if (notifyState == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We must have gotten a new tab that isn't the same as last time.
|
||||
if (notifyState != roundModify) {
|
||||
Svc.Prefs.set("notifyTabState", 0);
|
||||
}
|
||||
},
|
||||
|
||||
update: function (record) {
|
||||
|
|
|
@ -24,7 +24,6 @@ function test_create() {
|
|||
modified: 1000};
|
||||
store.applyIncoming(rec);
|
||||
deepEqual(store._remoteClients["id1"], { lastModified: 1000, foo: "bar" });
|
||||
equal(Svc.Prefs.get("notifyTabState"), 1);
|
||||
|
||||
_("Create a second record");
|
||||
rec = {id: "id2",
|
||||
|
@ -33,7 +32,6 @@ function test_create() {
|
|||
modified: 2000};
|
||||
store.applyIncoming(rec);
|
||||
deepEqual(store._remoteClients["id2"], { lastModified: 2000, foo2: "bar2" });
|
||||
equal(Svc.Prefs.get("notifyTabState"), 0);
|
||||
|
||||
_("Create a third record");
|
||||
rec = {id: "id3",
|
||||
|
@ -42,10 +40,6 @@ function test_create() {
|
|||
modified: 3000};
|
||||
store.applyIncoming(rec);
|
||||
deepEqual(store._remoteClients["id3"], { lastModified: 3000, foo3: "bar3" });
|
||||
equal(Svc.Prefs.get("notifyTabState"), 0);
|
||||
|
||||
// reset the notifyTabState
|
||||
Svc.Prefs.reset("notifyTabState");
|
||||
}
|
||||
|
||||
function test_getAllTabs() {
|
||||
|
|
|
@ -650,6 +650,9 @@ class XPCShellTestThread(Thread):
|
|||
self.env['DMD_PRELOAD_VAR'] = preloadEnvVar
|
||||
self.env['DMD_PRELOAD_VALUE'] = libdmd
|
||||
|
||||
if self.test_object.get('subprocess') == 'true':
|
||||
self.env['PYTHON'] = sys.executable
|
||||
|
||||
testTimeoutInterval = self.harness_timeout
|
||||
# Allow a test to request a multiple of the timeout if it is expected to take long
|
||||
if 'requesttimeoutfactor' in self.test_object:
|
||||
|
|
|
@ -54,10 +54,10 @@ Notification.prototype = {
|
|||
observe(subject, topic, data) {
|
||||
let notifications = notificationsMap.get(this.extension);
|
||||
|
||||
function emitAndDelete(event) {
|
||||
let emitAndDelete = event => {
|
||||
notifications.emit(event, data);
|
||||
notifications.delete(this.id);
|
||||
}
|
||||
};
|
||||
|
||||
// Don't try to emit events if the extension has been unloaded
|
||||
if (!notifications) {
|
||||
|
|
|
@ -34,13 +34,14 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|||
*/
|
||||
this.LoginHelper = {
|
||||
/**
|
||||
* Warning: this only updates if a logger was created.
|
||||
* Warning: these only update if a logger was created.
|
||||
*/
|
||||
debug: Services.prefs.getBoolPref("signon.debug"),
|
||||
schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
|
||||
|
||||
createLogger(aLogPrefix) {
|
||||
let getMaxLogLevel = () => {
|
||||
return this.debug ? "debug" : "error";
|
||||
return this.debug ? "debug" : "warn";
|
||||
};
|
||||
|
||||
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
|
||||
|
@ -54,6 +55,7 @@ this.LoginHelper = {
|
|||
// Watch for pref changes and update this.debug and the maxLogLevel for created loggers
|
||||
Services.prefs.addObserver("signon.", () => {
|
||||
this.debug = Services.prefs.getBoolPref("signon.debug");
|
||||
this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
|
||||
logger.maxLogLevel = getMaxLogLevel();
|
||||
}, false);
|
||||
|
||||
|
@ -133,6 +135,79 @@ this.LoginHelper = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a new XPCOM property bag with the provided properties.
|
||||
*
|
||||
* @param {Object} aProperties
|
||||
* Each property of this object is copied to the property bag. This
|
||||
* parameter can be omitted to return an empty property bag.
|
||||
*
|
||||
* @return A new property bag, that is an instance of nsIWritablePropertyBag,
|
||||
* nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
|
||||
*/
|
||||
newPropertyBag(aProperties) {
|
||||
let propertyBag = Cc["@mozilla.org/hash-property-bag;1"]
|
||||
.createInstance(Ci.nsIWritablePropertyBag);
|
||||
if (aProperties) {
|
||||
for (let [name, value] of Iterator(aProperties)) {
|
||||
propertyBag.setProperty(name, value);
|
||||
}
|
||||
}
|
||||
return propertyBag.QueryInterface(Ci.nsIPropertyBag)
|
||||
.QueryInterface(Ci.nsIPropertyBag2)
|
||||
.QueryInterface(Ci.nsIWritablePropertyBag2);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper to avoid the `count` argument and property bags when calling
|
||||
* Services.logins.searchLogins from JS.
|
||||
*
|
||||
* @param {Object} aSearchOptions - A regular JS object to copy to a property bag before searching
|
||||
* @return {nsILoginInfo[]} - The result of calling searchLogins.
|
||||
*/
|
||||
searchLoginsWithObject(aSearchOptions) {
|
||||
return Services.logins.searchLogins({}, this.newPropertyBag(aSearchOptions));
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {String} aLoginOrigin - An origin value from a stored login's
|
||||
* hostname or formSubmitURL properties.
|
||||
* @param {String} aSearchOrigin - The origin that was are looking to match
|
||||
* with aLoginOrigin. This would normally come
|
||||
* from a form or page that we are considering.
|
||||
* @param {nsILoginFindOptions} aOptions - Options to affect whether the origin
|
||||
* from the login (aLoginOrigin) is a
|
||||
* match for the origin we're looking
|
||||
* for (aSearchOrigin).
|
||||
*/
|
||||
isOriginMatching(aLoginOrigin, aSearchOrigin, aOptions = {
|
||||
schemeUpgrades: false,
|
||||
}) {
|
||||
if (aLoginOrigin == aSearchOrigin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!aOptions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aOptions.schemeUpgrades) {
|
||||
try {
|
||||
let loginURI = Services.io.newURI(aLoginOrigin, null, null);
|
||||
let searchURI = Services.io.newURI(aSearchOrigin, null, null);
|
||||
if (loginURI.scheme == "http" && searchURI.scheme == "https" &&
|
||||
loginURI.hostPort == searchURI.hostPort) {
|
||||
return true;
|
||||
}
|
||||
} catch (ex) {
|
||||
// newURI will throw for some values e.g. chrome://FirefoxAccounts
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new login object that results by modifying the given object with
|
||||
* the provided data.
|
||||
|
@ -260,38 +335,131 @@ this.LoginHelper = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Removes duplicates from a list of logins.
|
||||
* Removes duplicates from a list of logins while preserving the sort order.
|
||||
*
|
||||
* @param {nsILoginInfo[]} logins
|
||||
* A list of logins we want to deduplicate.
|
||||
*
|
||||
* @param {string[] = ["username", "password"]} uniqueKeys
|
||||
* @param {string[]} [uniqueKeys = ["username", "password"]]
|
||||
* A list of login attributes to use as unique keys for the deduplication.
|
||||
* @param {string[]} [resolveBy = ["timeLastUsed"]]
|
||||
* Ordered array of keyword strings used to decide which of the
|
||||
* duplicates should be used. "scheme" would prefer the login that has
|
||||
* a scheme matching `preferredOrigin`'s if there are two logins with
|
||||
* the same `uniqueKeys`. The default preference to distinguish two
|
||||
* logins is `timeLastUsed`. If there is no preference between two
|
||||
* logins, the first one found wins.
|
||||
* @param {string} [preferredOrigin = undefined]
|
||||
* String representing the origin to use for preferring one login over
|
||||
* another when they are dupes. This is used with "scheme" for
|
||||
* `resolveBy` so the scheme from this origin will be preferred.
|
||||
*
|
||||
* @returns {nsILoginInfo[]} list of unique logins.
|
||||
*/
|
||||
dedupeLogins(logins, uniqueKeys = ["username", "password"]) {
|
||||
dedupeLogins(logins, uniqueKeys = ["username", "password"],
|
||||
resolveBy = ["timeLastUsed"],
|
||||
preferredOrigin = undefined) {
|
||||
const KEY_DELIMITER = ":";
|
||||
|
||||
if (!preferredOrigin && resolveBy.includes("scheme")) {
|
||||
throw new Error("dedupeLogins: `preferredOrigin` is required in order to "+
|
||||
"prefer schemes which match it.");
|
||||
}
|
||||
|
||||
let preferredOriginScheme;
|
||||
if (preferredOrigin) {
|
||||
try {
|
||||
preferredOriginScheme = Services.io.newURI(preferredOrigin, null, null).scheme;
|
||||
} catch (ex) {
|
||||
// Handle strings that aren't valid URIs e.g. chrome://FirefoxAccounts
|
||||
}
|
||||
}
|
||||
|
||||
if (!preferredOriginScheme && resolveBy.includes("scheme")) {
|
||||
log.warn("dedupeLogins: Deduping with a scheme preference but couldn't " +
|
||||
"get the preferred origin scheme.");
|
||||
}
|
||||
|
||||
// We use a Map to easily lookup logins by their unique keys.
|
||||
let loginsByKeys = new Map();
|
||||
|
||||
// Generate a unique key string from a login.
|
||||
function getKey(login, uniqueKeys) {
|
||||
return uniqueKeys.reduce((prev, key) => prev + KEY_DELIMITER + login[key], "");
|
||||
}
|
||||
|
||||
// We use a Map to easily lookup logins by their unique keys.
|
||||
let loginsByKeys = new Map();
|
||||
/**
|
||||
* @return {bool} whether `login` is preferred over its duplicate (considering `uniqueKeys`)
|
||||
* `existingLogin`.
|
||||
*
|
||||
* `resolveBy` is a sorted array so we can return true the first time `login` is preferred
|
||||
* over the existingLogin.
|
||||
*/
|
||||
function isLoginPreferred(existingLogin, login) {
|
||||
if (!resolveBy || resolveBy.length == 0) {
|
||||
// If there is no preference, prefer the existing login.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let preference of resolveBy) {
|
||||
switch (preference) {
|
||||
case "scheme": {
|
||||
if (!preferredOriginScheme) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
// Only `hostname` is currently considered
|
||||
let existingLoginURI = Services.io.newURI(existingLogin.hostname, null, null);
|
||||
let loginURI = Services.io.newURI(login.hostname, null, null);
|
||||
// If the schemes of the two logins are the same or neither match the
|
||||
// preferredOriginScheme then we have no preference and look at the next resolveBy.
|
||||
if (loginURI.scheme == existingLoginURI.scheme ||
|
||||
(loginURI.scheme != preferredOriginScheme &&
|
||||
existingLoginURI.scheme != preferredOriginScheme)) {
|
||||
break;
|
||||
}
|
||||
|
||||
return loginURI.scheme == preferredOriginScheme;
|
||||
} catch (ex) {
|
||||
// Some URLs aren't valid nsIURI (e.g. chrome://FirefoxAccounts)
|
||||
log.debug("dedupeLogins/shouldReplaceExisting: Error comparing schemes:",
|
||||
existingLogin.hostname, login.hostname,
|
||||
"preferredOrigin:", preferredOrigin, ex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "timeLastUsed":
|
||||
case "timePasswordChanged": {
|
||||
// If we find a more recent login for the same key, replace the existing one.
|
||||
let loginDate = login.QueryInterface(Ci.nsILoginMetaInfo)[preference];
|
||||
let storedLoginDate = existingLogin.QueryInterface(Ci.nsILoginMetaInfo)[preference];
|
||||
if (loginDate == storedLoginDate) {
|
||||
break;
|
||||
}
|
||||
|
||||
return loginDate > storedLoginDate;
|
||||
}
|
||||
default: {
|
||||
throw new Error("dedupeLogins: Invalid resolveBy preference: " + preference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let login of logins) {
|
||||
let key = getKey(login, uniqueKeys);
|
||||
// If we find a more recently used login for the same key, replace the existing one.
|
||||
|
||||
if (loginsByKeys.has(key)) {
|
||||
let loginDate = login.QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
|
||||
let storedLoginDate = loginsByKeys.get(key).QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
|
||||
if (loginDate < storedLoginDate) {
|
||||
if (!isLoginPreferred(loginsByKeys.get(key), login)) {
|
||||
// If there is no preference for the new login, use the existing one.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
loginsByKeys.set(key, login);
|
||||
}
|
||||
|
||||
// Return the map values in the form of an array.
|
||||
return [...loginsByKeys.values()];
|
||||
},
|
||||
|
@ -418,14 +586,12 @@ this.LoginHelper = {
|
|||
*/
|
||||
loginToVanillaObject(login) {
|
||||
let obj = {};
|
||||
for (let i in login) {
|
||||
for (let i in login.QueryInterface(Ci.nsILoginMetaInfo)) {
|
||||
if (typeof login[i] !== 'function') {
|
||||
obj[i] = login[i];
|
||||
}
|
||||
}
|
||||
|
||||
login.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
obj.guid = login.guid;
|
||||
return obj;
|
||||
},
|
||||
|
||||
|
@ -433,15 +599,17 @@ this.LoginHelper = {
|
|||
* Convert an object received from IPC into an nsILoginInfo (with guid).
|
||||
*/
|
||||
vanillaObjectToLogin(login) {
|
||||
var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
|
||||
createInstance(Ci.nsILoginInfo);
|
||||
let formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
|
||||
createInstance(Ci.nsILoginInfo);
|
||||
formLogin.init(login.hostname, login.formSubmitURL,
|
||||
login.httpRealm, login.username,
|
||||
login.password, login.usernameField,
|
||||
login.passwordField);
|
||||
|
||||
formLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
formLogin.guid = login.guid;
|
||||
for (let prop of ["guid", "timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
|
||||
formLogin[prop] = login[prop];
|
||||
}
|
||||
return formLogin;
|
||||
},
|
||||
|
||||
|
@ -479,3 +647,8 @@ this.LoginHelper = {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
let logger = LoginHelper.createLogger("LoginHelper");
|
||||
return logger;
|
||||
});
|
||||
|
|
|
@ -229,6 +229,9 @@ var LoginManagerContent = {
|
|||
let win = doc.defaultView;
|
||||
|
||||
let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
|
||||
if (!formOrigin) {
|
||||
return Promise.reject("_getLoginDataFromParent: A form origin is required");
|
||||
}
|
||||
let actionOrigin = LoginUtils._getActionOrigin(form);
|
||||
|
||||
let messageManager = messageManagerFromWindow(win);
|
||||
|
|
|
@ -10,6 +10,8 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
|
||||
"resource://gre/modules/LoginManagerParent.jsm");
|
||||
|
||||
|
@ -93,7 +95,16 @@ var LoginManagerContextMenu = {
|
|||
* @returns {nsILoginInfo[]} a login list
|
||||
*/
|
||||
_findLogins(documentURI) {
|
||||
let logins = Services.logins.findLogins({}, documentURI.prePath, "", "");
|
||||
let searchParams = {
|
||||
hostname: documentURI.prePath,
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
};
|
||||
let logins = LoginHelper.searchLoginsWithObject(searchParams);
|
||||
let resolveBy = [
|
||||
"scheme",
|
||||
"timePasswordChanged",
|
||||
];
|
||||
logins = LoginHelper.dedupeLogins(logins, ["username", "password"], resolveBy, documentURI.prePath);
|
||||
|
||||
// Sort logins in alphabetical order and by date.
|
||||
logins.sort((loginA, loginB) => {
|
||||
|
|
|
@ -57,7 +57,6 @@ var LoginManagerParent = {
|
|||
});
|
||||
return this._recipeManager.initializationPromise;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
receiveMessage: function (msg) {
|
||||
|
@ -207,7 +206,17 @@ var LoginManagerParent = {
|
|||
return;
|
||||
}
|
||||
|
||||
var logins = Services.logins.findLogins({}, formOrigin, actionOrigin, null);
|
||||
let logins = LoginHelper.searchLoginsWithObject({
|
||||
formSubmitURL: actionOrigin,
|
||||
hostname: formOrigin,
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
});
|
||||
let resolveBy = [
|
||||
"scheme",
|
||||
"timePasswordChanged",
|
||||
];
|
||||
logins = LoginHelper.dedupeLogins(logins, ["username"], resolveBy, formOrigin);
|
||||
log("sendLoginDataToChild:", logins.length, "deduped logins");
|
||||
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
|
||||
// doesn't support structured cloning.
|
||||
var jsLogins = LoginHelper.loginsToVanillaObjects(logins);
|
||||
|
@ -238,7 +247,16 @@ var LoginManagerParent = {
|
|||
log("Creating new autocomplete search result.");
|
||||
|
||||
// Grab the logins from the database.
|
||||
logins = Services.logins.findLogins({}, formOrigin, actionOrigin, null);
|
||||
logins = LoginHelper.searchLoginsWithObject({
|
||||
formSubmitURL: actionOrigin,
|
||||
hostname: formOrigin,
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
});
|
||||
let resolveBy = [
|
||||
"scheme",
|
||||
"timePasswordChanged",
|
||||
];
|
||||
logins = LoginHelper.dedupeLogins(logins, ["username"], resolveBy, formOrigin);
|
||||
}
|
||||
|
||||
let matchingLogins = logins.filter(function(fullMatch) {
|
||||
|
@ -310,7 +328,11 @@ var LoginManagerParent = {
|
|||
(usernameField ? usernameField.name : ""),
|
||||
newPasswordField.name);
|
||||
|
||||
let logins = Services.logins.findLogins({}, hostname, formSubmitURL, null);
|
||||
let logins = LoginHelper.searchLoginsWithObject({
|
||||
formSubmitURL,
|
||||
hostname,
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
});
|
||||
|
||||
// If we didn't find a username field, but seem to be changing a
|
||||
// password, allow the user to select from a list of applicable
|
||||
|
@ -463,6 +485,7 @@ var LoginManagerParent = {
|
|||
}
|
||||
state.anchorDeferredTask.arm();
|
||||
},
|
||||
|
||||
updateLoginAnchor: Task.async(function* (browser) {
|
||||
// Copy the state to use for this execution of the task. These will not
|
||||
// change during this execution of the asynchronous function, but in case a
|
||||
|
@ -473,7 +496,11 @@ var LoginManagerParent = {
|
|||
|
||||
// Check if there are form logins for the site, ignoring formSubmitURL.
|
||||
let hasLogins = loginFormOrigin &&
|
||||
Services.logins.countLogins(loginFormOrigin, "", null) > 0;
|
||||
LoginHelper.searchLoginsWithObject({
|
||||
formSubmitURL: "",
|
||||
hostname: loginFormOrigin,
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
}).length > 0;
|
||||
|
||||
// Once this preference is removed, this version of the fill doorhanger
|
||||
// should be enabled for Desktop only, and not for Android or B2G.
|
||||
|
|
|
@ -250,7 +250,7 @@ var LoginRecipesContent = {
|
|||
}
|
||||
let field = aParent.ownerDocument.querySelector(aSelector);
|
||||
if (!field) {
|
||||
log.warn("Login field selector wasn't matched:", aSelector);
|
||||
log.debug("Login field selector wasn't matched:", aSelector);
|
||||
return null;
|
||||
}
|
||||
if (!(field instanceof aParent.ownerDocument.defaultView.HTMLInputElement)) {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
function nsLoginInfo() {}
|
||||
|
||||
|
@ -49,18 +48,8 @@ nsLoginInfo.prototype = {
|
|||
|
||||
// If either formSubmitURL is blank (but not null), then match.
|
||||
if (this.formSubmitURL != "" && aLogin.formSubmitURL != "" &&
|
||||
this.formSubmitURL != aLogin.formSubmitURL) {
|
||||
// If we have the same formSubmitURL hostPort we should match (ignore scheme)
|
||||
try {
|
||||
let loginURI = Services.io.newURI(aLogin.formSubmitURL, null, null);
|
||||
let matchURI = Services.io.newURI(this.formSubmitURL, null, null);
|
||||
if (loginURI.hostPort != matchURI.hostPort) {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.formSubmitURL != aLogin.formSubmitURL)
|
||||
return false;
|
||||
|
||||
// The .usernameField and .passwordField values are ignored.
|
||||
|
||||
|
|
|
@ -379,6 +379,15 @@ LoginManager.prototype = {
|
|||
searchLogins(count, matchData) {
|
||||
log.debug("Searching for logins");
|
||||
|
||||
matchData.QueryInterface(Ci.nsIPropertyBag2);
|
||||
if (!matchData.hasKey("hostname")) {
|
||||
log.warn("searchLogins: A `hostname` is recommended");
|
||||
}
|
||||
|
||||
if (!matchData.hasKey("formSubmitURL") && !matchData.hasKey("httpRealm")) {
|
||||
log.warn("searchLogins: `formSubmitURL` or `httpRealm` is recommended");
|
||||
}
|
||||
|
||||
return this._storage.searchLogins(count, matchData);
|
||||
},
|
||||
|
||||
|
|
|
@ -536,7 +536,7 @@ LoginManagerPrompter.prototype = {
|
|||
|
||||
// Looks for existing logins to prefill the prompt with.
|
||||
var foundLogins = this._pwmgr.findLogins({},
|
||||
hostname, null, httpRealm);
|
||||
hostname, null, httpRealm);
|
||||
this.log("found " + foundLogins.length + " matching logins.");
|
||||
|
||||
// XXX Can't select from multiple accounts yet. (bug 227632)
|
||||
|
@ -721,6 +721,7 @@ LoginManagerPrompter.prototype = {
|
|||
*
|
||||
*/
|
||||
promptToSavePassword : function (aLogin) {
|
||||
this.log("promptToSavePassword");
|
||||
var notifyObj = this._getPopupNote() || this._getNotifyBox();
|
||||
if (notifyObj)
|
||||
this._showSaveLoginNotification(notifyObj, aLogin);
|
||||
|
@ -1175,6 +1176,7 @@ LoginManagerPrompter.prototype = {
|
|||
* The new login from the page form.
|
||||
*/
|
||||
promptToChangePassword(aOldLogin, aNewLogin) {
|
||||
this.log("promptToChangePassword");
|
||||
let notifyObj = this._getPopupNote() || this._getNotifyBox();
|
||||
|
||||
if (notifyObj) {
|
||||
|
@ -1302,6 +1304,7 @@ LoginManagerPrompter.prototype = {
|
|||
* Note; XPCOM stupidity: |count| is just |logins.length|.
|
||||
*/
|
||||
promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
|
||||
this.log("promptToChangePasswordWithUsernames with count:", count);
|
||||
const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
|
||||
|
||||
var usernames = logins.map(l => l.username);
|
||||
|
|
|
@ -244,14 +244,25 @@ this.LoginManagerStorage_json.prototype = {
|
|||
*/
|
||||
searchLogins(count, matchData) {
|
||||
let realMatchData = {};
|
||||
let options = {};
|
||||
// Convert nsIPropertyBag to normal JS object
|
||||
let propEnum = matchData.enumerator;
|
||||
while (propEnum.hasMoreElements()) {
|
||||
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
||||
realMatchData[prop.name] = prop.value;
|
||||
switch (prop.name) {
|
||||
// Some property names aren't field names but are special options to affect the search.
|
||||
case "schemeUpgrades": {
|
||||
options[prop.name] = prop.value;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
realMatchData[prop.name] = prop.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let [logins, ids] = this._searchLogins(realMatchData);
|
||||
let [logins, ids] = this._searchLogins(realMatchData, options);
|
||||
|
||||
// Decrypt entries found for the caller.
|
||||
logins = this._decryptLogins(logins);
|
||||
|
@ -268,51 +279,38 @@ this.LoginManagerStorage_json.prototype = {
|
|||
* is an array of encrypted nsLoginInfo and ids is an array of associated
|
||||
* ids in the database.
|
||||
*/
|
||||
_searchLogins(matchData) {
|
||||
_searchLogins(matchData, aOptions = {
|
||||
schemeUpgrades: false,
|
||||
}) {
|
||||
this._store.ensureDataReady();
|
||||
|
||||
let conditions = [];
|
||||
|
||||
function match(aLogin) {
|
||||
let returnValue = {
|
||||
match: false,
|
||||
strictMatch: true
|
||||
};
|
||||
|
||||
for (let field in matchData) {
|
||||
let value = matchData[field];
|
||||
let wantedValue = matchData[field];
|
||||
switch (field) {
|
||||
// Historical compatibility requires this special case
|
||||
case "formSubmitURL":
|
||||
if (value != null) {
|
||||
if (aLogin.formSubmitURL != "" && aLogin.formSubmitURL != value) {
|
||||
// Check for cases that don't have fallback matches.
|
||||
if (value == "" || value == "javascript:" ||
|
||||
aLogin.formSubmitURL == "javascript:" ||
|
||||
aLogin.formSubmitURL == null) {
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
// Check if it matches with a different scheme.
|
||||
let loginURI = Services.io.newURI(aLogin.formSubmitURL, null, null);
|
||||
let matchURI = Services.io.newURI(value, null, null);
|
||||
|
||||
if (loginURI.hostPort != matchURI.hostPort) {
|
||||
return returnValue; // not a match at all
|
||||
}
|
||||
|
||||
if ((loginURI.scheme != "http" && loginURI.scheme != "https") ||
|
||||
(matchURI.scheme != "http" && matchURI.scheme != "https")) {
|
||||
// Not a match at all since we only fallback HTTP <=> HTTPS.
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
returnValue.strictMatch = false; // not a strict match
|
||||
if (wantedValue != null) {
|
||||
// Historical compatibility requires this special case
|
||||
if (aLogin.formSubmitURL == "") {
|
||||
break;
|
||||
}
|
||||
if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Normal cases.
|
||||
// fall through
|
||||
case "hostname":
|
||||
if (wantedValue != null) { // needed for formSubmitURL fall through
|
||||
if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// fall through
|
||||
// Normal cases.
|
||||
case "httpRealm":
|
||||
case "id":
|
||||
case "usernameField":
|
||||
|
@ -325,10 +323,10 @@ this.LoginManagerStorage_json.prototype = {
|
|||
case "timeLastUsed":
|
||||
case "timePasswordChanged":
|
||||
case "timesUsed":
|
||||
if (value == null && aLogin[field]) {
|
||||
return returnValue;
|
||||
} else if (aLogin[field] != value) {
|
||||
return returnValue;
|
||||
if (wantedValue == null && aLogin[field]) {
|
||||
return false;
|
||||
} else if (aLogin[field] != wantedValue) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
// Fail if caller requests an unknown property.
|
||||
|
@ -336,14 +334,12 @@ this.LoginManagerStorage_json.prototype = {
|
|||
throw new Error("Unexpected field: " + field);
|
||||
}
|
||||
}
|
||||
returnValue.match = true;
|
||||
return returnValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
let foundLogins = [], foundIds = [], fallbackLogins = [], fallbackIds = [];
|
||||
let foundLogins = [], foundIds = [];
|
||||
for (let loginItem of this._store.data.logins) {
|
||||
let result = match(loginItem);
|
||||
if (result.match) {
|
||||
if (match(loginItem)) {
|
||||
// Create the new nsLoginInfo object, push to array
|
||||
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
|
||||
createInstance(Ci.nsILoginInfo);
|
||||
|
@ -358,22 +354,12 @@ this.LoginManagerStorage_json.prototype = {
|
|||
login.timeLastUsed = loginItem.timeLastUsed;
|
||||
login.timePasswordChanged = loginItem.timePasswordChanged;
|
||||
login.timesUsed = loginItem.timesUsed;
|
||||
// If protocol does not match, use as a fallback login
|
||||
if (result.strictMatch) {
|
||||
foundLogins.push(login);
|
||||
foundIds.push(loginItem.id);
|
||||
} else {
|
||||
fallbackLogins.push(login);
|
||||
fallbackIds.push(loginItem.id);
|
||||
}
|
||||
foundLogins.push(login);
|
||||
foundIds.push(loginItem.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundLogins.length && fallbackLogins.length) {
|
||||
this.log("_searchLogins: returning", fallbackLogins.length, "fallback logins");
|
||||
return [fallbackLogins, fallbackIds];
|
||||
}
|
||||
this.log("_searchLogins: returning", foundLogins.length, "logins");
|
||||
this.log("_searchLogins: returning", foundLogins.length, "logins for", matchData);
|
||||
return [foundLogins, foundIds];
|
||||
},
|
||||
|
||||
|
|
|
@ -421,24 +421,33 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||
},
|
||||
|
||||
|
||||
/*
|
||||
* searchLogins
|
||||
*
|
||||
/**
|
||||
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
|
||||
* JavaScript object and decrypt the results.
|
||||
*
|
||||
* Returns an array of decrypted nsILoginInfo.
|
||||
* @return {nsILoginInfo[]} which are decrypted.
|
||||
*/
|
||||
searchLogins : function(count, matchData) {
|
||||
let realMatchData = {};
|
||||
let options = {};
|
||||
// Convert nsIPropertyBag to normal JS object
|
||||
let propEnum = matchData.enumerator;
|
||||
while (propEnum.hasMoreElements()) {
|
||||
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
||||
realMatchData[prop.name] = prop.value;
|
||||
switch (prop.name) {
|
||||
// Some property names aren't field names but are special options to affect the search.
|
||||
case "schemeUpgrades": {
|
||||
options[prop.name] = prop.value;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
realMatchData[prop.name] = prop.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let [logins, ids] = this._searchLogins(realMatchData);
|
||||
let [logins, ids] = this._searchLogins(realMatchData, options);
|
||||
|
||||
// Decrypt entries found for the caller.
|
||||
logins = this._decryptLogins(logins);
|
||||
|
@ -448,9 +457,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||
},
|
||||
|
||||
|
||||
/*
|
||||
* _searchLogins
|
||||
*
|
||||
/**
|
||||
* Private method to perform arbitrary searches on any field. Decryption is
|
||||
* left to the caller.
|
||||
*
|
||||
|
@ -458,21 +465,40 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||
* is an array of encrypted nsLoginInfo and ids is an array of associated
|
||||
* ids in the database.
|
||||
*/
|
||||
_searchLogins : function (matchData) {
|
||||
_searchLogins : function (matchData, aOptions = {
|
||||
schemeUpgrades: false,
|
||||
}) {
|
||||
let conditions = [], params = {};
|
||||
|
||||
for (let field in matchData) {
|
||||
let value = matchData[field];
|
||||
let condition = "";
|
||||
switch (field) {
|
||||
// Historical compatibility requires this special case
|
||||
case "formSubmitURL":
|
||||
if (value != null) {
|
||||
// As we also need to check for different schemes at the URI
|
||||
// this case gets handled by filtering the result of the query.
|
||||
break;
|
||||
// Historical compatibility requires this special case
|
||||
condition = "formSubmitURL = '' OR ";
|
||||
}
|
||||
// Normal cases.
|
||||
// Fall through
|
||||
case "hostname":
|
||||
if (value != null) {
|
||||
condition += `${field} = :${field}`;
|
||||
params[field] = value;
|
||||
let valueURI;
|
||||
try {
|
||||
if (aOptions.schemeUpgrades && (valueURI = Services.io.newURI(value, null, null)) &&
|
||||
valueURI.scheme == "https") {
|
||||
condition += ` OR ${field} = :http${field}`;
|
||||
params["http" + field] = "http://" + valueURI.hostPort;
|
||||
}
|
||||
} catch (ex) {
|
||||
// newURI will throw for some values (e.g. chrome://FirefoxAccounts)
|
||||
// but those URLs wouldn't support upgrades anyways.
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Fall through
|
||||
// Normal cases.
|
||||
case "httpRealm":
|
||||
case "id":
|
||||
case "usernameField":
|
||||
|
@ -486,16 +512,19 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||
case "timePasswordChanged":
|
||||
case "timesUsed":
|
||||
if (value == null) {
|
||||
conditions.push(field + " isnull");
|
||||
condition = field + " isnull";
|
||||
} else {
|
||||
conditions.push(field + " = :" + field);
|
||||
params[field] = value;
|
||||
condition = field + " = :" + field;
|
||||
params[field] = value;
|
||||
}
|
||||
break;
|
||||
// Fail if caller requests an unknown property.
|
||||
default:
|
||||
throw new Error("Unexpected field: " + field);
|
||||
}
|
||||
if (condition) {
|
||||
conditions.push(condition);
|
||||
}
|
||||
}
|
||||
|
||||
// Build query
|
||||
|
@ -506,7 +535,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||
}
|
||||
|
||||
let stmt;
|
||||
let logins = [], ids = [], fallbackLogins = [], fallbackIds = [];
|
||||
let logins = [], ids = [];
|
||||
try {
|
||||
stmt = this._dbCreateStatement(query, params);
|
||||
// We can't execute as usual here, since we're iterating over rows
|
||||
|
@ -525,24 +554,8 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||
login.timeLastUsed = stmt.row.timeLastUsed;
|
||||
login.timePasswordChanged = stmt.row.timePasswordChanged;
|
||||
login.timesUsed = stmt.row.timesUsed;
|
||||
|
||||
if (login.formSubmitURL == "" || typeof(matchData.formSubmitURL) == "undefined" ||
|
||||
login.formSubmitURL == matchData.formSubmitURL) {
|
||||
logins.push(login);
|
||||
ids.push(stmt.row.id);
|
||||
} else if (login.formSubmitURL != null &&
|
||||
login.formSubmitURL != "javascript:" &&
|
||||
matchData.formSubmitURL != "javascript:") {
|
||||
let loginURI = Services.io.newURI(login.formSubmitURL, null, null);
|
||||
let matchURI = Services.io.newURI(matchData.formSubmitURL, null, null);
|
||||
|
||||
if (loginURI.hostPort == matchURI.hostPort &&
|
||||
((loginURI.scheme == "http" && matchURI.scheme == "https") ||
|
||||
(loginURI.scheme == "https" && matchURI.scheme == "http"))) {
|
||||
fallbackLogins.push(login);
|
||||
fallbackIds.push(stmt.row.id);
|
||||
}
|
||||
}
|
||||
logins.push(login);
|
||||
ids.push(stmt.row.id);
|
||||
}
|
||||
} catch (e) {
|
||||
this.log("_searchLogins failed: " + e.name + " : " + e.message);
|
||||
|
@ -552,10 +565,6 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
if (!logins.length && fallbackLogins.length) {
|
||||
this.log("_searchLogins: returning " + fallbackLogins.length + " fallback logins");
|
||||
return [fallbackLogins, fallbackIds];
|
||||
}
|
||||
this.log("_searchLogins: returning " + logins.length + " logins");
|
||||
return [logins, ids];
|
||||
},
|
||||
|
@ -733,20 +742,6 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||
};
|
||||
|
||||
let resultLogins = _countLoginsHelper(hostname, formSubmitURL, httpRealm);
|
||||
if (resultLogins == 0 && formSubmitURL != null &&
|
||||
formSubmitURL != "" && formSubmitURL != "javascript:") {
|
||||
let formSubmitURI = Services.io.newURI(formSubmitURL, null, null);
|
||||
let newScheme = null;
|
||||
if (formSubmitURI.scheme == "http") {
|
||||
newScheme = "https";
|
||||
} else if (formSubmitURI.scheme == "https") {
|
||||
newScheme = "http";
|
||||
}
|
||||
if (newScheme) {
|
||||
let newFormSubmitURL = newScheme + "://" + formSubmitURI.hostPort;
|
||||
resultLogins = _countLoginsHelper(hostname, newFormSubmitURL, httpRealm);
|
||||
}
|
||||
}
|
||||
this.log("_countLogins: counted logins: " + resultLogins);
|
||||
return resultLogins;
|
||||
},
|
||||
|
|
|
@ -71,15 +71,6 @@ this.LoginTestUtils = {
|
|||
Assert.ok(expected.every(e => actual.some(a => a.equals(e))));
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that every login in "expected" matches one in "actual".
|
||||
* The comparison uses the "matches" method of nsILoginInfo.
|
||||
*/
|
||||
assertLoginListsMatches(actual, expected, ignorePassword) {
|
||||
Assert.equal(expected.length, actual.length);
|
||||
Assert.ok(expected.every(e => actual.some(a => a.matches(e, ignorePassword))));
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the two provided arrays of strings contain the same values,
|
||||
* maybe in a different order, case-sensitively.
|
||||
|
@ -175,6 +166,9 @@ this.LoginTestUtils.testData = {
|
|||
new LoginInfo("http://www3.example.com", "http://www.example.com", null,
|
||||
"the username", "the password",
|
||||
"form_field_username", "form_field_password"),
|
||||
new LoginInfo("http://www3.example.com", "https://www.example.com", null,
|
||||
"the username", "the password",
|
||||
"form_field_username", "form_field_password"),
|
||||
new LoginInfo("http://www3.example.com", "http://example.com", null,
|
||||
"the username", "the password",
|
||||
"form_field_username", "form_field_password"),
|
||||
|
|
|
@ -16,7 +16,7 @@ add_task(function* test_initialize() {
|
|||
Services.prefs.setBoolPref("signon.autofillForms", false);
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("signon.autofillForms");
|
||||
Services.logins.removeAllLogins();
|
||||
Services.prefs.clearUserPref("signon.schemeUpgrades");
|
||||
});
|
||||
for (let login of loginList()) {
|
||||
Services.logins.addLogin(login);
|
||||
|
@ -27,7 +27,8 @@ add_task(function* test_initialize() {
|
|||
* Check if the context menu is populated with the right
|
||||
* menuitems for the target password input field.
|
||||
*/
|
||||
add_task(function* test_context_menu_populate_password() {
|
||||
add_task(function* test_context_menu_populate_password_noSchemeUpgrades() {
|
||||
Services.prefs.setBoolPref("signon.schemeUpgrades", false);
|
||||
yield BrowserTestUtils.withNewTab({
|
||||
gBrowser,
|
||||
url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
|
||||
|
@ -38,7 +39,30 @@ add_task(function* test_context_menu_populate_password() {
|
|||
|
||||
// Check the content of the password manager popup
|
||||
let popupMenu = document.getElementById("fill-login-popup");
|
||||
checkMenu(popupMenu);
|
||||
checkMenu(popupMenu, 2);
|
||||
|
||||
let contextMenu = document.getElementById("contentAreaContextMenu");
|
||||
contextMenu.hidePopup();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if the context menu is populated with the right
|
||||
* menuitems for the target password input field.
|
||||
*/
|
||||
add_task(function* test_context_menu_populate_password_schemeUpgrades() {
|
||||
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
|
||||
yield BrowserTestUtils.withNewTab({
|
||||
gBrowser,
|
||||
url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
|
||||
}, function* (browser) {
|
||||
let passwordInput = browser.contentWindow.document.getElementById("test-password-1");
|
||||
|
||||
yield openPasswordContextMenu(browser, passwordInput);
|
||||
|
||||
// Check the content of the password manager popup
|
||||
let popupMenu = document.getElementById("fill-login-popup");
|
||||
checkMenu(popupMenu, 3);
|
||||
|
||||
let contextMenu = document.getElementById("contentAreaContextMenu");
|
||||
contextMenu.hidePopup();
|
||||
|
@ -49,7 +73,8 @@ add_task(function* test_context_menu_populate_password() {
|
|||
* Check if the context menu is populated with the right menuitems
|
||||
* for the target username field with a password field present.
|
||||
*/
|
||||
add_task(function* test_context_menu_populate_username_with_password() {
|
||||
add_task(function* test_context_menu_populate_username_with_password_noSchemeUpgrades() {
|
||||
Services.prefs.setBoolPref("signon.schemeUpgrades", false);
|
||||
yield BrowserTestUtils.withNewTab({
|
||||
gBrowser,
|
||||
url: TEST_HOSTNAME + "/browser/toolkit/components/" +
|
||||
|
@ -61,7 +86,30 @@ add_task(function* test_context_menu_populate_username_with_password() {
|
|||
|
||||
// Check the content of the password manager popup
|
||||
let popupMenu = document.getElementById("fill-login-popup");
|
||||
checkMenu(popupMenu);
|
||||
checkMenu(popupMenu, 2);
|
||||
|
||||
let contextMenu = document.getElementById("contentAreaContextMenu");
|
||||
contextMenu.hidePopup();
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Check if the context menu is populated with the right menuitems
|
||||
* for the target username field with a password field present.
|
||||
*/
|
||||
add_task(function* test_context_menu_populate_username_with_password_schemeUpgrades() {
|
||||
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
|
||||
yield BrowserTestUtils.withNewTab({
|
||||
gBrowser,
|
||||
url: TEST_HOSTNAME + "/browser/toolkit/components/" +
|
||||
"passwordmgr/test/browser/multiple_forms.html",
|
||||
}, function* (browser) {
|
||||
let passwordInput = browser.contentWindow.document.getElementById("test-username-2");
|
||||
|
||||
yield openPasswordContextMenu(browser, passwordInput);
|
||||
|
||||
// Check the content of the password manager popup
|
||||
let popupMenu = document.getElementById("fill-login-popup");
|
||||
checkMenu(popupMenu, 3);
|
||||
|
||||
let contextMenu = document.getElementById("contentAreaContextMenu");
|
||||
contextMenu.hidePopup();
|
||||
|
@ -73,6 +121,7 @@ add_task(function* test_context_menu_populate_username_with_password() {
|
|||
* login menuitem is clicked.
|
||||
*/
|
||||
add_task(function* test_context_menu_password_fill() {
|
||||
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
|
||||
yield BrowserTestUtils.withNewTab({
|
||||
gBrowser,
|
||||
url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
|
||||
|
@ -111,7 +160,8 @@ add_task(function* test_context_menu_password_fill() {
|
|||
// The only field affected by the password fill
|
||||
// should be the target password field itself.
|
||||
let unchangedFields = form.querySelectorAll('input:not(#' + passwordField.id + ')');
|
||||
yield assertContextMenuFill(form, null, passwordField, unchangedFields);
|
||||
yield assertContextMenuFill(form, null, passwordField, unchangedFields, 1);
|
||||
Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used");
|
||||
contextMenu.hidePopup();
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +173,7 @@ add_task(function* test_context_menu_password_fill() {
|
|||
* username context menu login menuitem is clicked.
|
||||
*/
|
||||
add_task(function* test_context_menu_username_login_fill() {
|
||||
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
|
||||
yield BrowserTestUtils.withNewTab({
|
||||
gBrowser,
|
||||
url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
|
||||
|
@ -168,7 +219,10 @@ add_task(function* test_context_menu_username_login_fill() {
|
|||
}
|
||||
// We shouldn't change any field that's not the target username field or the first password field
|
||||
let unchangedFields = form.querySelectorAll('input:not(#' + usernameField.id + '):not(#' + passwordField.id + ')');
|
||||
yield assertContextMenuFill(form, usernameField, passwordField, unchangedFields);
|
||||
yield assertContextMenuFill(form, usernameField, passwordField, unchangedFields, 1);
|
||||
if (!passwordField.hasAttribute("expectedFail")) {
|
||||
Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used");
|
||||
}
|
||||
contextMenu.hidePopup();
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +233,7 @@ add_task(function* test_context_menu_username_login_fill() {
|
|||
* Check if the password field is correctly filled when it's in an iframe.
|
||||
*/
|
||||
add_task(function* test_context_menu_iframe_fill() {
|
||||
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
|
||||
yield BrowserTestUtils.withNewTab({
|
||||
gBrowser,
|
||||
url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
|
||||
|
@ -262,7 +317,7 @@ function* openPasswordContextMenu(browser, passwordInput, assertCallback = null)
|
|||
/**
|
||||
* Verify that only the expected form fields are filled.
|
||||
*/
|
||||
function* assertContextMenuFill(form, usernameField, passwordField, unchangedFields){
|
||||
function* assertContextMenuFill(form, usernameField, passwordField, unchangedFields, loginIndex){
|
||||
let popupMenu = document.getElementById("fill-login-popup");
|
||||
|
||||
// Store the value of fields that should remain unchanged.
|
||||
|
@ -272,14 +327,14 @@ function* assertContextMenuFill(form, usernameField, passwordField, unchangedFie
|
|||
}
|
||||
}
|
||||
|
||||
// Execute the default command of the first login menuitem found at the context menu.
|
||||
let firstLoginItem = popupMenu.getElementsByClassName("context-login-item")[0];
|
||||
firstLoginItem.doCommand();
|
||||
// Execute the default command of the specified login menuitem found in the context menu.
|
||||
let loginItem = popupMenu.getElementsByClassName("context-login-item")[loginIndex];
|
||||
loginItem.doCommand();
|
||||
|
||||
yield BrowserTestUtils.waitForEvent(form, "input", "Username input value changed");
|
||||
|
||||
// Find the used login by it's username (Use only unique usernames in this test).
|
||||
let login = getLoginFromUsername(firstLoginItem.label);
|
||||
let login = getLoginFromUsername(loginItem.label);
|
||||
|
||||
// If we have an username field, check if it's correctly filled
|
||||
if (usernameField && usernameField.getAttribute("expectedFail") == null) {
|
||||
|
@ -306,12 +361,19 @@ function* assertContextMenuFill(form, usernameField, passwordField, unchangedFie
|
|||
|
||||
/**
|
||||
* Check if every login that matches the page hostname are available at the context menu.
|
||||
* @param {Element} contextMenu
|
||||
* @param {Number} expectedCount - Number of logins expected in the context menu. Used to ensure
|
||||
* we continue testing something useful.
|
||||
*/
|
||||
function checkMenu(contextMenu) {
|
||||
let logins = loginList().filter(login => login.hostname == TEST_HOSTNAME);
|
||||
function checkMenu(contextMenu, expectedCount) {
|
||||
let logins = loginList().filter(login => {
|
||||
return LoginHelper.isOriginMatching(login.hostname, TEST_HOSTNAME, {
|
||||
schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
|
||||
});
|
||||
});
|
||||
// Make an array of menuitems for easier comparison.
|
||||
let menuitems = [...contextMenu.getElementsByClassName("context-login-item")];
|
||||
Assert.equal(menuitems.length, logins.length, "Same amount of menu items and expected logins.");
|
||||
Assert.equal(menuitems.length, expectedCount, "Expected number of menu items");
|
||||
Assert.ok(logins.every(l => menuitems.some(m => l.username == m.label)), "Every login have an item at the menu.");
|
||||
}
|
||||
|
||||
|
@ -328,7 +390,8 @@ function getLoginFromUsername(username) {
|
|||
* List of logins used for the test.
|
||||
*
|
||||
* We should only use unique usernames in this test,
|
||||
* because we need to search logins by username.
|
||||
* because we need to search logins by username. There is one duplicate u+p combo
|
||||
* in order to test de-duping in the menu.
|
||||
*/
|
||||
function loginList() {
|
||||
return [
|
||||
|
@ -338,6 +401,13 @@ function loginList() {
|
|||
username: "username",
|
||||
password: "password",
|
||||
}),
|
||||
// Same as above but HTTP in order to test de-duping.
|
||||
LoginTestUtils.testData.formLogin({
|
||||
hostname: "http://example.com",
|
||||
formSubmitURL: "http://example.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
}),
|
||||
LoginTestUtils.testData.formLogin({
|
||||
hostname: "http://example.com",
|
||||
formSubmitURL: "http://example.com",
|
||||
|
@ -351,8 +421,8 @@ function loginList() {
|
|||
password: "password2",
|
||||
}),
|
||||
LoginTestUtils.testData.formLogin({
|
||||
hostname: "https://example.org",
|
||||
formSubmitURL: "https://example.org",
|
||||
hostname: "http://example.org",
|
||||
formSubmitURL: "http://example.org",
|
||||
username: "username-cross-origin",
|
||||
password: "password-cross-origin",
|
||||
}),
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
[DEFAULT]
|
||||
skip-if = buildapp == 'mulet' || buildapp == 'b2g'
|
||||
support-files =
|
||||
../../../prompts/test/chromeScript.js
|
||||
../../../prompts/test/prompt_common.js
|
||||
../../../satchel/test/parent_utils.js
|
||||
../../../satchel/test/satchel_common.js
|
||||
../authenticate.sjs
|
||||
../pwmgr_common.js
|
||||
../browser/form_basic.html
|
||||
../browser/form_cross_origin_secure_action.html
|
||||
../notification_common.js
|
||||
../pwmgr_common.js
|
||||
auth2/authenticate.sjs
|
||||
../../../prompts/test/prompt_common.js
|
||||
../../../prompts/test/chromeScript.js
|
||||
|
||||
[test_autocomplete_https_upgrade.html]
|
||||
skip-if = toolkit == 'android' # autocomplete
|
||||
[test_autofill_https_upgrade.html]
|
||||
skip-if = toolkit == 'android' # Bug 1259768
|
||||
[test_autofill_password-only.html]
|
||||
[test_basic_form.html]
|
||||
[test_basic_form_0pw.html]
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="satchel_common.js"></script>
|
||||
<script type="text/javascript" src="pwmgr_common.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const chromeScript = runChecksAfterCommonInit(false);
|
||||
|
||||
runInParent(function addLogins() {
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
// Create some logins just for this form, since we'll be deleting them.
|
||||
let nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
|
||||
Ci.nsILoginInfo, "init");
|
||||
|
||||
// We have two actual HTTPS to avoid autofill before the schemeUpgrades pref flips to true.
|
||||
let login0 = new nsLoginInfo("https://example.org", "https://example.org", null,
|
||||
"name", "pass", "uname", "pword");
|
||||
|
||||
let login1 = new nsLoginInfo("https://example.org", "https://example.org", null,
|
||||
"name1", "pass1", "uname", "pword");
|
||||
|
||||
// Same as above but HTTP instead of HTTPS (to test de-duping)
|
||||
let login2 = new nsLoginInfo("http://example.org", "http://example.org", null,
|
||||
"name1", "passHTTP", "uname", "pword");
|
||||
|
||||
// Different HTTP login to upgrade with secure formSubmitURL
|
||||
let login3 = new nsLoginInfo("http://example.org", "https://example.org", null,
|
||||
"name2", "passHTTPtoHTTPS", "uname", "pword");
|
||||
|
||||
try {
|
||||
Services.logins.addLogin(login0);
|
||||
Services.logins.addLogin(login1);
|
||||
Services.logins.addLogin(login2);
|
||||
Services.logins.addLogin(login3);
|
||||
} catch (e) {
|
||||
assert.ok(false, "addLogin threw: " + e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<p id="display"></p>
|
||||
|
||||
<!-- we presumably can't hide the content for this test. -->
|
||||
<div id="content">
|
||||
<iframe src="https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html"></iframe>
|
||||
</div>
|
||||
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
|
||||
|
||||
let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
|
||||
let iframeDoc;
|
||||
let uname;
|
||||
let pword;
|
||||
|
||||
// Restore the form to the default state.
|
||||
function restoreForm() {
|
||||
pword.focus();
|
||||
uname.value = "";
|
||||
pword.value = "";
|
||||
uname.focus();
|
||||
}
|
||||
|
||||
// Check for expected username/password in form.
|
||||
function checkACForm(expectedUsername, expectedPassword) {
|
||||
let formID = uname.parentNode.id;
|
||||
is(uname.value, expectedUsername, "Checking " + formID + " username");
|
||||
is(pword.value, expectedPassword, "Checking " + formID + " password");
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
yield SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]});
|
||||
|
||||
yield new Promise(resolve => {
|
||||
iframe.addEventListener("load", function onLoad() {
|
||||
iframe.removeEventListener("load", onLoad);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
iframeDoc = iframe.contentDocument;
|
||||
uname = iframeDoc.getElementById("form-basic-username");
|
||||
pword = iframeDoc.getElementById("form-basic-password");
|
||||
});
|
||||
|
||||
add_task(function* test_empty_first_entry() {
|
||||
// Make sure initial form is empty.
|
||||
checkACForm("", "");
|
||||
// Trigger autocomplete popup
|
||||
restoreForm();
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup is initially closed");
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down");
|
||||
let results = yield shownPromise;
|
||||
popupState = yield getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entries are selected");
|
||||
checkArrayValues(results, ["name", "name1", "name2"], "initial");
|
||||
|
||||
// Check first entry
|
||||
let index0Promise = notifySelectedIndex(0);
|
||||
doKey("down");
|
||||
yield index0Promise;
|
||||
checkACForm("", ""); // value shouldn't update
|
||||
doKey("return"); // not "enter"!
|
||||
yield promiseFormsProcessed();
|
||||
checkACForm("name", "pass");
|
||||
});
|
||||
|
||||
add_task(function* test_empty_second_entry() {
|
||||
restoreForm();
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down"); // open
|
||||
yield shownPromise;
|
||||
doKey("down"); // first
|
||||
doKey("down"); // second
|
||||
doKey("return"); // not "enter"!
|
||||
yield promiseFormsProcessed();
|
||||
checkACForm("name1", "pass1");
|
||||
});
|
||||
|
||||
add_task(function* test_search() {
|
||||
restoreForm();
|
||||
let shownPromise = promiseACShown();
|
||||
// We need to blur for the autocomplete controller to notice the forced value below.
|
||||
uname.blur();
|
||||
uname.value = "name";
|
||||
uname.focus();
|
||||
sendChar("1");
|
||||
doKey("down"); // open
|
||||
let results = yield shownPromise;
|
||||
checkArrayValues(results, ["name1"], "check result deduping for 'name1'");
|
||||
doKey("down"); // first
|
||||
doKey("return"); // not "enter"!
|
||||
yield promiseFormsProcessed();
|
||||
checkACForm("name1", "pass1");
|
||||
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup is now closed");
|
||||
});
|
||||
|
||||
add_task(function* test_delete_first_entry() {
|
||||
restoreForm();
|
||||
uname.focus();
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down");
|
||||
yield shownPromise;
|
||||
|
||||
let index0Promise = notifySelectedIndex(0);
|
||||
doKey("down");
|
||||
yield index0Promise;
|
||||
|
||||
let deletionPromise = promiseStorageChanged(["removeLogin"]);
|
||||
// On OS X, shift-backspace and shift-delete work, just delete does not.
|
||||
// On Win/Linux, shift-backspace does not work, delete and shift-delete do.
|
||||
doKey("delete", shiftModifier);
|
||||
yield deletionPromise;
|
||||
checkACForm("", "");
|
||||
|
||||
let results = yield notifyMenuChanged(2, "name1");
|
||||
|
||||
checkArrayValues(results, ["name1", "name2"], "two should remain after deleting the first");
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, true, "Check popup stays open after deleting");
|
||||
doKey("escape");
|
||||
popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup closed upon ESC");
|
||||
});
|
||||
|
||||
add_task(function* test_delete_duplicate_entry() {
|
||||
restoreForm();
|
||||
uname.focus();
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down");
|
||||
yield shownPromise;
|
||||
|
||||
let index0Promise = notifySelectedIndex(0);
|
||||
doKey("down");
|
||||
yield index0Promise;
|
||||
|
||||
let deletionPromise = promiseStorageChanged(["removeLogin"]);
|
||||
// On OS X, shift-backspace and shift-delete work, just delete does not.
|
||||
// On Win/Linux, shift-backspace does not work, delete and shift-delete do.
|
||||
doKey("delete", shiftModifier);
|
||||
yield deletionPromise;
|
||||
checkACForm("", "");
|
||||
|
||||
is(LoginManager.countLogins("http://example.org", "http://example.org", null), 1,
|
||||
"Check that the HTTP login remains");
|
||||
is(LoginManager.countLogins("https://example.org", "https://example.org", null), 0,
|
||||
"Check that the HTTPS login was deleted");
|
||||
|
||||
// Two menu items should remain as the HTTPS login should have been deleted but
|
||||
// the HTTP would remain.
|
||||
let results = yield notifyMenuChanged(1, "name2");
|
||||
|
||||
checkArrayValues(results, ["name2"], "one should remain after deleting the HTTPS name1");
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, true, "Check popup stays open after deleting");
|
||||
doKey("escape");
|
||||
popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup closed upon ESC");
|
||||
});
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,117 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="satchel_common.js"></script>
|
||||
<script type="text/javascript" src="pwmgr_common.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
|
||||
const CROSS_ORIGIN_SECURE_PATH = TESTS_DIR + "mochitest/form_cross_origin_secure_action.html";
|
||||
|
||||
const chromeScript = runChecksAfterCommonInit(false);
|
||||
|
||||
let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
|
||||
SpecialPowers.Ci.nsILoginInfo,
|
||||
"init");
|
||||
</script>
|
||||
<p id="display"></p>
|
||||
|
||||
<!-- we presumably can't hide the content for this test. -->
|
||||
<div id="content">
|
||||
<iframe></iframe>
|
||||
</div>
|
||||
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
|
||||
|
||||
// Check for expected username/password in form.
|
||||
function checkACForm(expectedUsername, expectedPassword) {
|
||||
let iframeDoc = iframe.contentDocument;
|
||||
let uname = iframeDoc.getElementById("form-basic-username");
|
||||
let pword = iframeDoc.getElementById("form-basic-password");
|
||||
let formID = uname.parentNode.id;
|
||||
is(uname.value, expectedUsername, "Checking " + formID + " username");
|
||||
is(pword.value, expectedPassword, "Checking " + formID + " password");
|
||||
}
|
||||
function* prepareLoginsAndProcessForm(url, logins = []) {
|
||||
LoginManager.removeAllLogins();
|
||||
|
||||
let dates = Date.now();
|
||||
for (let login of logins) {
|
||||
SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo);
|
||||
// Force all dates to be the same so they don't affect things like deduping.
|
||||
login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates;
|
||||
LoginManager.addLogin(login);
|
||||
}
|
||||
|
||||
iframe.src = url;
|
||||
yield promiseFormsProcessed();
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
yield SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]});
|
||||
})
|
||||
|
||||
add_task(function* test_simpleNoDupesNoAction() {
|
||||
yield prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
|
||||
new nsLoginInfo("http://example.com", "http://example.com", null,
|
||||
"name2", "pass2", "uname", "pword"),
|
||||
]);
|
||||
|
||||
checkACForm("name2", "pass2");
|
||||
});
|
||||
|
||||
add_task(function* test_simpleNoDupesUpgradeOriginAndAction() {
|
||||
yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
|
||||
new nsLoginInfo("http://example.com", "http://another.domain", null,
|
||||
"name2", "pass2", "uname", "pword"),
|
||||
]);
|
||||
|
||||
checkACForm("name2", "pass2");
|
||||
});
|
||||
|
||||
add_task(function* test_simpleNoDupesUpgradeOriginOnly() {
|
||||
yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
|
||||
new nsLoginInfo("http://example.com", "https://another.domain", null,
|
||||
"name2", "pass2", "uname", "pword"),
|
||||
]);
|
||||
|
||||
checkACForm("name2", "pass2");
|
||||
});
|
||||
|
||||
add_task(function* test_simpleNoDupesUpgradeActionOnly() {
|
||||
yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
|
||||
new nsLoginInfo("https://example.com", "http://another.domain", null,
|
||||
"name2", "pass2", "uname", "pword"),
|
||||
]);
|
||||
|
||||
checkACForm("name2", "pass2");
|
||||
});
|
||||
|
||||
add_task(function* test_dedupe() {
|
||||
yield prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
|
||||
new nsLoginInfo("https://example.com", "https://example.com", null,
|
||||
"name1", "passHTTPStoHTTPS", "uname", "pword"),
|
||||
new nsLoginInfo("http://example.com", "http://example.com", null,
|
||||
"name1", "passHTTPtoHTTP", "uname", "pword"),
|
||||
new nsLoginInfo("http://example.com", "https://example.com", null,
|
||||
"name1", "passHTTPtoHTTPS", "uname", "pword"),
|
||||
new nsLoginInfo("https://example.com", "http://example.com", null,
|
||||
"name1", "passHTTPStoHTTP", "uname", "pword"),
|
||||
]);
|
||||
|
||||
checkACForm("name1", "passHTTPStoHTTPS");
|
||||
});
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -128,12 +128,6 @@ runChecksAfterCommonInit(() => startTest());
|
|||
<button type='submit'>Submit</button>
|
||||
</form>
|
||||
|
||||
<form id='form15' action='https://mochi.test:8888/tests/formtest.js'> 15
|
||||
<!-- Different scheme for same url, should be filled -->
|
||||
<input type='text' name='uname' value=''>
|
||||
<input type='password' name='pname' value=''>
|
||||
<button type='submit'>Submit</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -165,9 +159,6 @@ function startTest() {
|
|||
checkForm(f++, "xxxxxxxx", "testuser", "testpass");
|
||||
checkForm(f++, "testuser", "testpass", "xxxxxxxx");
|
||||
|
||||
//15
|
||||
checkForm(f++, "testuser", "testpass");
|
||||
|
||||
SimpleTest.finish();
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -56,7 +56,7 @@ function checkSubmit(formNum) {
|
|||
if (formNum == 999) {
|
||||
is(numSubmittedForms, 999, "Ensuring all forms submitted for testing.");
|
||||
|
||||
var numEndingLogins = countLogins();
|
||||
var numEndingLogins = LoginManager.countLogins("", "", "");
|
||||
|
||||
ok(numEndingLogins > 0, "counting logins at end");
|
||||
is(numStartingLogins, numEndingLogins + 222, "counting logins at end");
|
||||
|
|
|
@ -60,9 +60,6 @@ var setupScript = runInParent(function setup() {
|
|||
var login8C = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
|
||||
"form9userAABzz", "form9pass", "uname", "pword");
|
||||
|
||||
var login9 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete6", null,
|
||||
"testuser9", "testpass9", "uname", "pword");
|
||||
|
||||
var login10 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete7", null,
|
||||
"testuser10", "testpass10", "uname", "pword");
|
||||
|
||||
|
@ -81,7 +78,6 @@ var setupScript = runInParent(function setup() {
|
|||
Services.logins.addLogin(login8A);
|
||||
Services.logins.addLogin(login8B);
|
||||
// login8C is added later
|
||||
Services.logins.addLogin(login9);
|
||||
Services.logins.addLogin(login10);
|
||||
} catch (e) {
|
||||
assert.ok(false, "addLogin threw: " + e);
|
||||
|
@ -164,15 +160,8 @@ var setupScript = runInParent(function setup() {
|
|||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<!-- test for different scheme -->
|
||||
<form id="form10" action="https://autocomplete6" onsubmit="return false;">
|
||||
<input type="text" name="uname">
|
||||
<input type="password" name="pword">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<!-- test for onUsernameInput recipe testing -->
|
||||
<form id="form11" action="https://autocomplete7" onsubmit="return false;">
|
||||
<form id="form11" action="http://autocomplete7" onsubmit="return false;">
|
||||
<input type="text" name="1">
|
||||
<input type="text" name="2">
|
||||
<button type="submit">Submit</button>
|
||||
|
@ -230,7 +219,6 @@ add_task(function* test_form1_initial_empty() {
|
|||
checkACForm("", "");
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup is initially closed");
|
||||
is(popupState.selectedIndex, -1, "Check no entries are selected");
|
||||
});
|
||||
|
||||
add_task(function* test_form1_first_entry() {
|
||||
|
@ -240,6 +228,10 @@ add_task(function* test_form1_first_entry() {
|
|||
let shownPromise = promiseACShown();
|
||||
doKey("down"); // open
|
||||
yield shownPromise;
|
||||
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
|
||||
|
||||
doKey("down"); // first
|
||||
checkACForm("", ""); // value shouldn't update just by selecting
|
||||
doKey("return"); // not "enter"!
|
||||
|
@ -439,7 +431,7 @@ add_task(function* test_form1_delete() {
|
|||
// Delete the first entry (of 4), "tempuser1"
|
||||
doKey("down");
|
||||
var numLogins;
|
||||
numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
is(numLogins, 5, "Correct number of logins before deleting one");
|
||||
|
||||
var deletionPromise = promiseStorageChanged(["removeLogin"]);
|
||||
|
@ -449,7 +441,7 @@ add_task(function* test_form1_delete() {
|
|||
yield deletionPromise;
|
||||
|
||||
checkACForm("", "");
|
||||
numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
is(numLogins, 4, "Correct number of logins after deleting one");
|
||||
notifyMenuChanged(4);
|
||||
doKey("return");
|
||||
|
@ -481,7 +473,7 @@ add_task(function* test_form1_delete_second() {
|
|||
doKey("down");
|
||||
doKey("delete", shiftModifier);
|
||||
checkACForm("", "");
|
||||
numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
is(numLogins, 3, "Correct number of logins after deleting one");
|
||||
doKey("return");
|
||||
yield promiseFormsProcessed();
|
||||
|
@ -513,7 +505,7 @@ add_task(function* test_form1_delete_last() {
|
|||
doKey("down");
|
||||
doKey("delete", shiftModifier);
|
||||
checkACForm("", "");
|
||||
numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
is(numLogins, 2, "Correct number of logins after deleting one");
|
||||
doKey("return");
|
||||
yield promiseFormsProcessed();
|
||||
|
@ -545,7 +537,7 @@ add_task(function* test_form1_check_only_entry_remaining() {
|
|||
doKey("delete", shiftModifier);
|
||||
//doKey("return");
|
||||
checkACForm("", "");
|
||||
numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
|
||||
is(numLogins, 1, "Correct number of logins after deleting one");
|
||||
|
||||
// remove the login that's not shown in the list.
|
||||
|
@ -752,23 +744,6 @@ add_task(function* test_form9_autocomplete_cache() {
|
|||
is(popupState.open, false, "Check popup stays closed due to cached empty result");
|
||||
});
|
||||
|
||||
add_task(function* test_form10_formSubmitURLScheme() {
|
||||
// Check that formSubmitURL with different schemes matches
|
||||
uname = $_(10, "uname");
|
||||
pword = $_(10, "pword");
|
||||
restoreForm();
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down"); // open
|
||||
yield shownPromise;
|
||||
|
||||
// Check first entry
|
||||
doKey("down");
|
||||
checkACForm("", ""); // value shouldn't update
|
||||
doKey("return"); // not "enter"!
|
||||
yield promiseFormsProcessed();
|
||||
checkACForm("testuser9", "testpass9");
|
||||
});
|
||||
|
||||
add_task(function* test_form11_recipes() {
|
||||
yield loadRecipes({
|
||||
siteRecipes: [{
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/";
|
||||
|
||||
/**
|
||||
* Returns the element with the specified |name| attribute.
|
||||
*/
|
||||
|
@ -275,7 +277,7 @@ function promiseFormsProcessed(expectedCount = 1) {
|
|||
processedCount++;
|
||||
if (processedCount == expectedCount) {
|
||||
SpecialPowers.removeObserver(onProcessedForm, "passwordmgr-processed-form");
|
||||
resolve(subject, data);
|
||||
resolve(SpecialPowers.Cu.waiveXrays(subject), data);
|
||||
}
|
||||
}
|
||||
SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form", false);
|
||||
|
@ -318,10 +320,6 @@ function promiseStorageChanged(expectedChangeTypes) {
|
|||
});
|
||||
}
|
||||
|
||||
function countLogins(chromeScript, formOrigin, submitOrigin, httpRealm) {
|
||||
return chromeScript.sendSyncMessage("countLogins", {formOrigin, submitOrigin, httpRealm})[0][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a function synchronously in the parent process and destroy it in the test cleanup function.
|
||||
* @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
|
||||
|
@ -361,6 +359,7 @@ if (this.addMessageListener) {
|
|||
// Ignore ok/is in commonInit since they aren't defined in a chrome script.
|
||||
ok = is = () => {}; // eslint-disable-line no-native-reassign
|
||||
|
||||
Cu.import("resource://gre/modules/LoginHelper.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
|
@ -391,8 +390,17 @@ if (this.addMessageListener) {
|
|||
sendAsyncMessage("recipesReset");
|
||||
}));
|
||||
|
||||
addMessageListener("countLogins", ({formOrigin, submitOrigin, httpRealm}) => {
|
||||
return Services.logins.countLogins(formOrigin, submitOrigin, httpRealm);
|
||||
addMessageListener("proxyLoginManager", msg => {
|
||||
// Recreate nsILoginInfo objects from vanilla JS objects.
|
||||
let recreatedArgs = msg.args.map((arg, index) => {
|
||||
if (msg.loginInfoIndices.includes(index)) {
|
||||
return LoginHelper.vanillaObjectToLogin(arg);
|
||||
}
|
||||
|
||||
return arg;
|
||||
});
|
||||
|
||||
return Services.logins[msg.methodName](...recreatedArgs);
|
||||
});
|
||||
|
||||
var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
|
||||
|
@ -422,4 +430,33 @@ if (this.addMessageListener) {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
let { LoginHelper } = SpecialPowers.Cu.import("resource://gre/modules/LoginHelper.jsm", {});
|
||||
/**
|
||||
* Proxy for Services.logins (nsILoginManager).
|
||||
* Only supports arguments which support structured clone plus {nsILoginInfo}
|
||||
* Assumes properties are methods.
|
||||
*/
|
||||
this.LoginManager = new Proxy({}, {
|
||||
get(target, prop, receiver) {
|
||||
return (...args) => {
|
||||
let loginInfoIndices = [];
|
||||
let cloneableArgs = args.map((val, index) => {
|
||||
if (SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)) {
|
||||
loginInfoIndices.push(index);
|
||||
return LoginHelper.loginToVanillaObject(val);
|
||||
}
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
return chromeScript.sendSyncMessage("proxyLoginManager", {
|
||||
args: cloneableArgs,
|
||||
loginInfoIndices,
|
||||
methodName: prop,
|
||||
})[0][0];
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Provides infrastructure for automated login components tests.
|
||||
*/
|
||||
|
@ -12,7 +7,7 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
@ -37,6 +32,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "LoginTestUtils",
|
|||
"resource://testing-common/LoginTestUtils.jsm");
|
||||
LoginTestUtils.Assert = Assert;
|
||||
const TestData = LoginTestUtils.testData;
|
||||
const newPropertyBag = LoginHelper.newPropertyBag;
|
||||
|
||||
/**
|
||||
* All the tests are implemented with add_task, this starts them automatically.
|
||||
|
@ -57,7 +53,7 @@ function run_test()
|
|||
// used, on Windows these might still be pending deletion on the physical file
|
||||
// system. Thus, start from a new base number every time, to make a collision
|
||||
// with a file that is still pending deletion highly unlikely.
|
||||
var gFileCounter = Math.floor(Math.random() * 1000000);
|
||||
let gFileCounter = Math.floor(Math.random() * 1000000);
|
||||
|
||||
/**
|
||||
* Returns a reference to a temporary file, that is guaranteed not to exist, and
|
||||
|
@ -93,30 +89,6 @@ function getTempFile(aLeafName)
|
|||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new XPCOM property bag with the provided properties.
|
||||
*
|
||||
* @param aProperties
|
||||
* Each property of this object is copied to the property bag. This
|
||||
* parameter can be omitted to return an empty property bag.
|
||||
*
|
||||
* @return A new property bag, that is an instance of nsIWritablePropertyBag,
|
||||
* nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
|
||||
*/
|
||||
function newPropertyBag(aProperties)
|
||||
{
|
||||
let propertyBag = Cc["@mozilla.org/hash-property-bag;1"]
|
||||
.createInstance(Ci.nsIWritablePropertyBag);
|
||||
if (aProperties) {
|
||||
for (let [name, value] of Iterator(aProperties)) {
|
||||
propertyBag.setProperty(name, value);
|
||||
}
|
||||
}
|
||||
return propertyBag.QueryInterface(Ci.nsIPropertyBag)
|
||||
.QueryInterface(Ci.nsIPropertyBag2)
|
||||
.QueryInterface(Ci.nsIWritablePropertyBag2);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const RecipeHelpers = {
|
||||
|
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
* Test LoginHelper.dedupeLogins
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/LoginHelper.jsm");
|
||||
|
||||
const DOMAIN1_HTTP_TO_HTTP_U1_P1 = TestData.formLogin({
|
||||
timePasswordChanged: 3000,
|
||||
timeLastUsed: 2000,
|
||||
});
|
||||
const DOMAIN1_HTTP_TO_HTTP_U1_P2 = TestData.formLogin({
|
||||
password: "password two",
|
||||
});
|
||||
const DOMAIN1_HTTP_TO_HTTP_U2_P2 = TestData.formLogin({
|
||||
password: "password two",
|
||||
username: "username two",
|
||||
});
|
||||
const DOMAIN1_HTTPS_TO_HTTPS_U1_P1 = TestData.formLogin({
|
||||
formSubmitURL: "http://www.example.com",
|
||||
hostname: "https://www3.example.com",
|
||||
timePasswordChanged: 4000,
|
||||
timeLastUsed: 1000,
|
||||
});
|
||||
const DOMAIN1_HTTPS_TO_EMPTY_U1_P1 = TestData.formLogin({
|
||||
formSubmitURL: "",
|
||||
hostname: "https://www3.example.com",
|
||||
});
|
||||
const DOMAIN1_HTTPS_TO_EMPTYU_P1 = TestData.formLogin({
|
||||
hostname: "https://www3.example.com",
|
||||
username: "",
|
||||
});
|
||||
const DOMAIN1_HTTP_AUTH = TestData.authLogin({
|
||||
hostname: "http://www3.example.com",
|
||||
});
|
||||
const DOMAIN1_HTTPS_AUTH = TestData.authLogin({
|
||||
hostname: "https://www3.example.com",
|
||||
});
|
||||
|
||||
|
||||
add_task(function test_dedupeLogins() {
|
||||
// [description, expectedOutput, dedupe arg. 0, dedupe arg 1, ...]
|
||||
let testcases = [
|
||||
[
|
||||
"exact dupes",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
[], // force no resolveBy logic to test behavior of preferring the first..
|
||||
],
|
||||
[
|
||||
"default uniqueKeys is un + pw",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
|
||||
undefined,
|
||||
[],
|
||||
],
|
||||
[
|
||||
"same usernames, different passwords, dedupe username only",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
|
||||
["username"],
|
||||
[],
|
||||
],
|
||||
[
|
||||
"same un+pw, different scheme",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
undefined,
|
||||
[],
|
||||
],
|
||||
[
|
||||
"same un+pw, different scheme, reverse order",
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
[],
|
||||
],
|
||||
[
|
||||
"same un+pw, different scheme, include hostname",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
["hostname", "username", "password"],
|
||||
[],
|
||||
],
|
||||
[
|
||||
"empty username is not deduped with non-empty",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_EMPTYU_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_EMPTYU_P1],
|
||||
undefined,
|
||||
[],
|
||||
],
|
||||
[
|
||||
"empty username is deduped with same passwords",
|
||||
[DOMAIN1_HTTPS_TO_EMPTYU_P1],
|
||||
[DOMAIN1_HTTPS_TO_EMPTYU_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
["password"],
|
||||
[],
|
||||
],
|
||||
[
|
||||
"mix of form and HTTP auth",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_AUTH],
|
||||
undefined,
|
||||
[],
|
||||
],
|
||||
];
|
||||
|
||||
for (let tc of testcases) {
|
||||
let description = tc.shift();
|
||||
let expected = tc.shift();
|
||||
let actual = LoginHelper.dedupeLogins(...tc);
|
||||
Assert.strictEqual(actual.length, expected.length, `Check: ${description}`);
|
||||
for (let [i, login] of expected.entries()) {
|
||||
Assert.strictEqual(actual[i], login, `Check index ${i}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_dedupeLogins_resolveBy() {
|
||||
Assert.ok(DOMAIN1_HTTP_TO_HTTP_U1_P1.timeLastUsed > DOMAIN1_HTTPS_TO_HTTPS_U1_P1.timeLastUsed,
|
||||
"Sanity check timeLastUsed difference");
|
||||
Assert.ok(DOMAIN1_HTTP_TO_HTTP_U1_P1.timePasswordChanged < DOMAIN1_HTTPS_TO_HTTPS_U1_P1.timePasswordChanged,
|
||||
"Sanity check timePasswordChanged difference");
|
||||
|
||||
let testcases = [
|
||||
[
|
||||
"default resolveBy is timeLastUsed",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
],
|
||||
[
|
||||
"default resolveBy is timeLastUsed, reversed input",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
],
|
||||
[
|
||||
"resolveBy timeLastUsed + timePasswordChanged",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
["timeLastUsed", "timePasswordChanged"],
|
||||
],
|
||||
[
|
||||
"resolveBy timeLastUsed + timePasswordChanged, reversed input",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
undefined,
|
||||
["timeLastUsed", "timePasswordChanged"],
|
||||
],
|
||||
[
|
||||
"resolveBy timePasswordChanged",
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
["timePasswordChanged"],
|
||||
],
|
||||
[
|
||||
"resolveBy timePasswordChanged, reversed",
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
undefined,
|
||||
["timePasswordChanged"],
|
||||
],
|
||||
[
|
||||
"resolveBy timePasswordChanged + timeLastUsed",
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
["timePasswordChanged", "timeLastUsed"],
|
||||
],
|
||||
[
|
||||
"resolveBy timePasswordChanged + timeLastUsed, reversed",
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
undefined,
|
||||
["timePasswordChanged", "timeLastUsed"],
|
||||
],
|
||||
[
|
||||
"resolveBy scheme + timePasswordChanged, prefer HTTP",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
["scheme", "timePasswordChanged"],
|
||||
DOMAIN1_HTTP_TO_HTTP_U1_P1.hostname,
|
||||
],
|
||||
[
|
||||
"resolveBy scheme + timePasswordChanged, prefer HTTP, reversed input",
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
undefined,
|
||||
["scheme", "timePasswordChanged"],
|
||||
DOMAIN1_HTTP_TO_HTTP_U1_P1.hostname,
|
||||
],
|
||||
[
|
||||
"resolveBy scheme + timePasswordChanged, prefer HTTPS",
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
["scheme", "timePasswordChanged"],
|
||||
DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
|
||||
],
|
||||
[
|
||||
"resolveBy scheme + timePasswordChanged, prefer HTTPS, reversed input",
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
undefined,
|
||||
["scheme", "timePasswordChanged"],
|
||||
DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
|
||||
],
|
||||
[
|
||||
"resolveBy scheme HTTP auth",
|
||||
[DOMAIN1_HTTPS_AUTH],
|
||||
[DOMAIN1_HTTP_AUTH, DOMAIN1_HTTPS_AUTH],
|
||||
undefined,
|
||||
["scheme"],
|
||||
DOMAIN1_HTTPS_AUTH.hostname,
|
||||
],
|
||||
[
|
||||
"resolveBy scheme HTTP auth, reversed input",
|
||||
[DOMAIN1_HTTPS_AUTH],
|
||||
[DOMAIN1_HTTPS_AUTH, DOMAIN1_HTTP_AUTH],
|
||||
undefined,
|
||||
["scheme"],
|
||||
DOMAIN1_HTTPS_AUTH.hostname,
|
||||
],
|
||||
[
|
||||
"resolveBy scheme, empty form submit URL",
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTPS_TO_EMPTY_U1_P1],
|
||||
undefined,
|
||||
["scheme"],
|
||||
DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
|
||||
],
|
||||
];
|
||||
|
||||
for (let tc of testcases) {
|
||||
let description = tc.shift();
|
||||
let expected = tc.shift();
|
||||
let actual = LoginHelper.dedupeLogins(...tc);
|
||||
Assert.strictEqual(actual.length, expected.length, `Check: ${description}`);
|
||||
for (let [i, login] of expected.entries()) {
|
||||
Assert.strictEqual(actual[i], login, `Check index ${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
add_task(function* test_dedupeLogins_preferredOriginMissing() {
|
||||
let testcases = [
|
||||
[
|
||||
"resolveBy scheme + timePasswordChanged, missing preferredOrigin",
|
||||
/preferredOrigin/,
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
["scheme", "timePasswordChanged"],
|
||||
],
|
||||
[
|
||||
"resolveBy timePasswordChanged + scheme, missing preferredOrigin",
|
||||
/preferredOrigin/,
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
["timePasswordChanged", "scheme"],
|
||||
],
|
||||
[
|
||||
"resolveBy scheme + timePasswordChanged, empty preferredOrigin",
|
||||
/preferredOrigin/,
|
||||
[DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
|
||||
undefined,
|
||||
["scheme", "timePasswordChanged"],
|
||||
"",
|
||||
],
|
||||
];
|
||||
|
||||
for (let tc of testcases) {
|
||||
let description = tc.shift();
|
||||
let expectedException = tc.shift();
|
||||
Assert.throws(() => {
|
||||
LoginHelper.dedupeLogins(...tc);
|
||||
}, expectedException, `Check: ${description}`);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Test LoginHelper.isOriginMatching
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/LoginHelper.jsm");
|
||||
|
||||
add_task(function test_isOriginMatching() {
|
||||
let testcases = [
|
||||
// Index 0 holds the expected return value followed by arguments to isOriginMatching.
|
||||
[true, "http://example.com", "http://example.com"],
|
||||
[true, "http://example.com:8080", "http://example.com:8080"],
|
||||
[true, "https://example.com", "https://example.com"],
|
||||
[true, "https://example.com:8443", "https://example.com:8443"],
|
||||
[false, "http://example.com", "http://mozilla.org"],
|
||||
[false, "http://example.com", "http://example.com:8080"],
|
||||
[false, "https://example.com", "http://example.com"],
|
||||
[false, "https://example.com", "https://mozilla.org"],
|
||||
[false, "http://example.com", "http://sub.example.com"],
|
||||
[false, "https://example.com", "https://sub.example.com"],
|
||||
[false, "http://example.com", "https://example.com:8443"],
|
||||
[false, "http://example.com:8080", "http://example.com:8081"],
|
||||
[false, "http://example.com", ""],
|
||||
[false, "", "http://example.com"],
|
||||
[true, "http://example.com", "https://example.com", { schemeUpgrades: true }],
|
||||
[true, "https://example.com", "https://example.com", { schemeUpgrades: true }],
|
||||
[true, "http://example.com:8080", "http://example.com:8080", { schemeUpgrades: true }],
|
||||
[true, "https://example.com:8443", "https://example.com:8443", { schemeUpgrades: true }],
|
||||
[false, "https://example.com", "http://example.com", { schemeUpgrades: true }], // downgrade
|
||||
[false, "http://example.com:8080", "https://example.com", { schemeUpgrades: true }], // port mismatch
|
||||
[false, "http://example.com", "https://example.com:8443", { schemeUpgrades: true }], // port mismatch
|
||||
[false, "http://sub.example.com", "http://example.com", { schemeUpgrades: true }],
|
||||
];
|
||||
for (let tc of testcases) {
|
||||
let expected = tc.shift();
|
||||
Assert.strictEqual(LoginHelper.isOriginMatching(...tc), expected,
|
||||
"Check " + JSON.stringify(tc));
|
||||
}
|
||||
});
|
|
@ -340,7 +340,7 @@ add_task(function test_deduplicate_logins() {
|
|||
},
|
||||
{
|
||||
keyset: ["hostname", "username", "password", "formSubmitURL"],
|
||||
results: 22,
|
||||
results: 23,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
/*
|
||||
* Tests methods that find specific logins in the store (findLogins,
|
||||
* searchLogins, and countLogins).
|
||||
*
|
||||
|
@ -97,55 +92,6 @@ function checkAllSearches(aQuery, aExpectedCount)
|
|||
checkSearchLogins(aQuery, aExpectedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests findLogins, searchLogins, and countLogins with a different set of
|
||||
* queries for the search and expected resultset.
|
||||
*
|
||||
* @param {Object} aQuery
|
||||
* The "hostname", "formSubmitURL", and "httpRealm" properties of this
|
||||
* object are passed as parameters to findLogins, countLogins
|
||||
* and searchLogins function.
|
||||
* @param {Object} buildQuery
|
||||
* The "hostname", "formSubmitURL", and "httpRealm" properties of the
|
||||
* object used to build the expected logins to have as a result.
|
||||
* @param {Number} aExpectedCount
|
||||
* Number of logins from the test data that should be found. The actual
|
||||
* list of logins is obtained using the buildExpectedLogins helper, and
|
||||
* this value is just used to verify that modifications to the test data
|
||||
* don't make the current test meaningless.
|
||||
*/
|
||||
function checkAllSearchesTwoSets(aQuery, expectedQuery, aExpectedCount)
|
||||
{
|
||||
do_print("Testing all search functions for " + JSON.stringify(aQuery) +
|
||||
" and " + JSON.stringify(expectedQuery));
|
||||
|
||||
let expectedLogins = buildExpectedLogins(expectedQuery);
|
||||
|
||||
// The findLogins and countLogins functions support wildcard matches by
|
||||
// specifying empty strings as parameters, while searchLogins requires
|
||||
// omitting the property entirely.
|
||||
let hostname = ("hostname" in aQuery) ? aQuery.hostname : "";
|
||||
let formSubmitURL = ("formSubmitURL" in aQuery) ? aQuery.formSubmitURL : "";
|
||||
let httpRealm = ("httpRealm" in aQuery) ? aQuery.httpRealm : "";
|
||||
|
||||
// Test findLogins.
|
||||
let outCount = {};
|
||||
let logins = Services.logins.findLogins(outCount, hostname, formSubmitURL,
|
||||
httpRealm);
|
||||
do_check_eq(outCount.value, expectedLogins.length);
|
||||
LoginTestUtils.assertLoginListsMatches(logins, expectedLogins, true);
|
||||
|
||||
// Test countLogins.
|
||||
let count = Services.logins.countLogins(hostname, formSubmitURL, httpRealm)
|
||||
do_check_eq(count, expectedLogins.length);
|
||||
|
||||
// Test searchLogins.
|
||||
outCount = {};
|
||||
logins = Services.logins.searchLogins(outCount, newPropertyBag(expectedQuery));
|
||||
do_check_eq(outCount.value, expectedLogins.length);
|
||||
LoginTestUtils.assertLoginListsMatches(logins, expectedLogins, true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Tests
|
||||
|
||||
|
@ -165,10 +111,10 @@ add_task(function test_initialize()
|
|||
add_task(function test_search_all_basic()
|
||||
{
|
||||
// Find all logins, using no filters in the search functions.
|
||||
checkAllSearches({}, 22);
|
||||
checkAllSearches({}, 23);
|
||||
|
||||
// Find all form logins, then all authentication logins.
|
||||
checkAllSearches({ httpRealm: null }, 13);
|
||||
checkAllSearches({ httpRealm: null }, 14);
|
||||
checkAllSearches({ formSubmitURL: null }, 9);
|
||||
|
||||
// Find all form logins on one host, then all authentication logins.
|
||||
|
@ -181,16 +127,18 @@ add_task(function test_search_all_basic()
|
|||
checkAllSearches({ hostname: "http://www.example.com" }, 1);
|
||||
checkAllSearches({ hostname: "https://www.example.com" }, 1);
|
||||
checkAllSearches({ hostname: "https://example.com" }, 1);
|
||||
checkAllSearches({ hostname: "http://www3.example.com" }, 2);
|
||||
checkAllSearches({ hostname: "http://www3.example.com" }, 3);
|
||||
|
||||
// Verify that scheme and subdomain are distinct in formSubmitURL.
|
||||
checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2);
|
||||
checkAllSearches({ formSubmitURL: "https://www.example.com" }, 1);
|
||||
checkAllSearches({ formSubmitURL: "https://www.example.com" }, 2);
|
||||
checkAllSearches({ formSubmitURL: "http://example.com" }, 1);
|
||||
|
||||
// Find by formSubmitURL on a single host.
|
||||
checkAllSearches({ hostname: "http://www3.example.com",
|
||||
formSubmitURL: "http://www.example.com" }, 1);
|
||||
checkAllSearches({ hostname: "http://www3.example.com",
|
||||
formSubmitURL: "https://www.example.com" }, 1);
|
||||
checkAllSearches({ hostname: "http://www3.example.com",
|
||||
formSubmitURL: "http://example.com" }, 1);
|
||||
|
||||
|
@ -213,8 +161,8 @@ add_task(function test_search_all_basic()
|
|||
*/
|
||||
add_task(function test_searchLogins()
|
||||
{
|
||||
checkSearchLogins({ usernameField: "form_field_username" }, 11);
|
||||
checkSearchLogins({ passwordField: "form_field_password" }, 12);
|
||||
checkSearchLogins({ usernameField: "form_field_username" }, 12);
|
||||
checkSearchLogins({ passwordField: "form_field_password" }, 13);
|
||||
|
||||
// Find all logins with an empty usernameField, including for authentication.
|
||||
checkSearchLogins({ usernameField: "" }, 11);
|
||||
|
@ -250,8 +198,9 @@ add_task(function test_search_all_full_case_sensitive()
|
|||
checkAllSearches({ hostname: "example.com" }, 0);
|
||||
|
||||
checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2);
|
||||
checkAllSearches({ formSubmitURL: "http://www.example.com/" }, 0);
|
||||
checkAllSearches({ formSubmitURL: "http://" }, 0);
|
||||
Assert.throws(() => checkAllSearches({ formSubmitURL: "example.com" }, 0), /NS_ERROR_MALFORMED_URI/);
|
||||
checkAllSearches({ formSubmitURL: "example.com" }, 0);
|
||||
|
||||
checkAllSearches({ httpRealm: "The HTTP Realm" }, 3);
|
||||
checkAllSearches({ httpRealm: "The http Realm" }, 0);
|
||||
|
@ -272,19 +221,3 @@ add_task(function test_search_all_empty()
|
|||
checkSearchLogins({ hostname: "" }, 0);
|
||||
checkSearchLogins({ id: "1000" }, 0);
|
||||
});
|
||||
|
||||
|
||||
add_task(function test_search_different_formSubmitURL_scheme()
|
||||
{
|
||||
let aQuery = {
|
||||
formSubmitURL: "https://www.example.com",
|
||||
hostname: "http://www.example.com",
|
||||
};
|
||||
|
||||
let buildQuery = {
|
||||
formSubmitURL: "http://www.example.com",
|
||||
hostname: "http://www.example.com",
|
||||
}
|
||||
|
||||
checkAllSearchesTwoSets(aQuery, buildQuery, 1);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Test Services.logins.searchLogins with the `schemeUpgrades` property.
|
||||
*/
|
||||
|
||||
const HTTP3_ORIGIN = "http://www3.example.com";
|
||||
const HTTPS_ORIGIN = "https://www.example.com";
|
||||
const HTTP_ORIGIN = "http://www.example.com";
|
||||
|
||||
/**
|
||||
* Returns a list of new nsILoginInfo objects that are a subset of the test
|
||||
* data, built to match the specified query.
|
||||
*
|
||||
* @param {Object} aQuery
|
||||
* Each property and value of this object restricts the search to those
|
||||
* entries from the test data that match the property exactly.
|
||||
*/
|
||||
function buildExpectedLogins(aQuery) {
|
||||
return TestData.loginList().filter(
|
||||
entry => Object.keys(aQuery).every(name => {
|
||||
if (name == "schemeUpgrades") {
|
||||
return true;
|
||||
}
|
||||
if (["hostname", "formSubmitURL"].includes(name)) {
|
||||
return LoginHelper.isOriginMatching(entry[name], aQuery[name], {
|
||||
schemeUpgrades: aQuery.schemeUpgrades,
|
||||
});
|
||||
}
|
||||
return entry[name] === aQuery[name];
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the searchLogins function.
|
||||
*
|
||||
* @param {Object} aQuery
|
||||
* Each property and value of this object is translated to an entry in
|
||||
* the nsIPropertyBag parameter of searchLogins.
|
||||
* @param {Number} aExpectedCount
|
||||
* Number of logins from the test data that should be found. The actual
|
||||
* list of logins is obtained using the buildExpectedLogins helper, and
|
||||
* this value is just used to verify that modifications to the test data
|
||||
* don't make the current test meaningless.
|
||||
*/
|
||||
function checkSearch(aQuery, aExpectedCount) {
|
||||
do_print("Testing searchLogins for " + JSON.stringify(aQuery));
|
||||
|
||||
let expectedLogins = buildExpectedLogins(aQuery);
|
||||
do_check_eq(expectedLogins.length, aExpectedCount);
|
||||
|
||||
let outCount = {};
|
||||
let logins = Services.logins.searchLogins(outCount, newPropertyBag(aQuery));
|
||||
do_check_eq(outCount.value, expectedLogins.length);
|
||||
LoginTestUtils.assertLoginListsEqual(logins, expectedLogins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare data for the following tests.
|
||||
*/
|
||||
add_task(function test_initialize() {
|
||||
for (let login of TestData.loginList()) {
|
||||
Services.logins.addLogin(login);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests searchLogins with the `schemeUpgrades` property
|
||||
*/
|
||||
add_task(function test_search_schemeUpgrades_hostname() {
|
||||
// Hostname-only
|
||||
checkSearch({
|
||||
hostname: HTTPS_ORIGIN,
|
||||
}, 1);
|
||||
checkSearch({
|
||||
hostname: HTTPS_ORIGIN,
|
||||
schemeUpgrades: false,
|
||||
}, 1);
|
||||
checkSearch({
|
||||
hostname: HTTPS_ORIGIN,
|
||||
schemeUpgrades: undefined,
|
||||
}, 1);
|
||||
checkSearch({
|
||||
hostname: HTTPS_ORIGIN,
|
||||
schemeUpgrades: true,
|
||||
}, 2);
|
||||
});
|
||||
|
||||
/**
|
||||
* Same as above but replacing hostname with formSubmitURL.
|
||||
*/
|
||||
add_task(function test_search_schemeUpgrades_formSubmitURL() {
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
}, 2);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
schemeUpgrades: false,
|
||||
}, 2);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
schemeUpgrades: undefined,
|
||||
}, 2);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
schemeUpgrades: true,
|
||||
}, 4);
|
||||
});
|
||||
|
||||
|
||||
add_task(function test_search_schemeUpgrades_hostname_formSubmitURL() {
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTPS_ORIGIN,
|
||||
}, 1);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTPS_ORIGIN,
|
||||
schemeUpgrades: false,
|
||||
}, 1);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTPS_ORIGIN,
|
||||
schemeUpgrades: undefined,
|
||||
}, 1);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTPS_ORIGIN,
|
||||
schemeUpgrades: true,
|
||||
}, 2);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTPS_ORIGIN,
|
||||
schemeUpgrades: true,
|
||||
usernameField: "form_field_username",
|
||||
}, 2);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTPS_ORIGIN,
|
||||
passwordField: "form_field_password",
|
||||
schemeUpgrades: true,
|
||||
usernameField: "form_field_username",
|
||||
}, 2);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTPS_ORIGIN,
|
||||
httpRealm: null,
|
||||
passwordField: "form_field_password",
|
||||
schemeUpgrades: true,
|
||||
usernameField: "form_field_username",
|
||||
}, 2);
|
||||
});
|
||||
|
||||
/**
|
||||
* HTTP submitting to HTTPS
|
||||
*/
|
||||
add_task(function test_http_to_https() {
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTP3_ORIGIN,
|
||||
httpRealm: null,
|
||||
schemeUpgrades: false,
|
||||
}, 1);
|
||||
checkSearch({
|
||||
formSubmitURL: HTTPS_ORIGIN,
|
||||
hostname: HTTP3_ORIGIN,
|
||||
httpRealm: null,
|
||||
schemeUpgrades: true,
|
||||
}, 2);
|
||||
});
|
||||
|
||||
/**
|
||||
* schemeUpgrades shouldn't cause downgrades
|
||||
*/
|
||||
add_task(function test_search_schemeUpgrades_downgrade() {
|
||||
checkSearch({
|
||||
formSubmitURL: HTTP_ORIGIN,
|
||||
hostname: HTTP_ORIGIN,
|
||||
}, 1);
|
||||
do_print("The same number should be found with schemeUpgrades since we're searching for HTTP");
|
||||
checkSearch({
|
||||
formSubmitURL: HTTP_ORIGIN,
|
||||
hostname: HTTP_ORIGIN,
|
||||
schemeUpgrades: true,
|
||||
}, 1);
|
||||
});
|
|
@ -21,10 +21,12 @@ skip-if = true || os != "android" # Bug 1171687: Needs fixing on Android
|
|||
skip-if = os == "android" # The context menu isn't used on Android.
|
||||
# LoginManagerContextMenu is only included for MOZ_BUILD_APP == 'browser'.
|
||||
run-if = buildapp == "browser"
|
||||
[test_dedupeLogins.js]
|
||||
[test_disabled_hosts.js]
|
||||
[test_getFormFields.js]
|
||||
[test_getPasswordFields.js]
|
||||
[test_getPasswordOrigin.js]
|
||||
[test_isOriginMatching.js]
|
||||
[test_legacy_empty_formSubmitURL.js]
|
||||
[test_legacy_validation.js]
|
||||
[test_logins_change.js]
|
||||
|
@ -37,5 +39,6 @@ skip-if = os == "android" # Bug 1171687: Needs fixing on Android
|
|||
skip-if = os != "win"
|
||||
[test_recipes_add.js]
|
||||
[test_recipes_content.js]
|
||||
[test_search_schemeUpgrades.js]
|
||||
[test_storage.js]
|
||||
[test_telemetry.js]
|
||||
|
|
|
@ -106,6 +106,10 @@ if 'Android' != CONFIG['OS_TARGET']:
|
|||
EXTRA_JS_MODULES += [
|
||||
'LightweightThemeConsumer.jsm',
|
||||
]
|
||||
|
||||
DIRS += [
|
||||
'subprocess',
|
||||
]
|
||||
else:
|
||||
DEFINES['ANDROID'] = True
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"extends": "../../components/extensions/.eslintrc",
|
||||
|
||||
"env": {
|
||||
"worker": true,
|
||||
},
|
||||
|
||||
"globals": {
|
||||
"ChromeWorker": false,
|
||||
"Components": false,
|
||||
"LIBC": true,
|
||||
"Library": true,
|
||||
"OS": false,
|
||||
"Services": false,
|
||||
"SubprocessConstants": true,
|
||||
"ctypes": false,
|
||||
"debug": true,
|
||||
"dump": false,
|
||||
"libc": true,
|
||||
"unix": true,
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"no-console": 0,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
/*
|
||||
* These modules are loosely based on the subprocess.jsm module created
|
||||
* by Jan Gerber and Patrick Brunschwig, though the implementation
|
||||
* differs drastically.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
let EXPORTED_SYMBOLS = ["Subprocess"];
|
||||
|
||||
/* exported Subprocess */
|
||||
|
||||
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/subprocess/subprocess_common.jsm");
|
||||
|
||||
if (AppConstants.platform == "win") {
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SubprocessImpl",
|
||||
"resource://gre/modules/subprocess/subprocess_win.jsm");
|
||||
} else {
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SubprocessImpl",
|
||||
"resource://gre/modules/subprocess/subprocess_unix.jsm");
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for creation of and communication with OS-level sub-processes.
|
||||
* @namespace
|
||||
*/
|
||||
var Subprocess = {
|
||||
/**
|
||||
* Launches a process, and returns a handle to it.
|
||||
*
|
||||
* @param {object} options
|
||||
* An object describing the process to launch.
|
||||
*
|
||||
* @param {string} options.command
|
||||
* The full path of the execuable to launch. Relative paths are not
|
||||
* accepted, and `$PATH` is not searched.
|
||||
*
|
||||
* If a path search is necessary, the {@link Subprocess.pathSearch} method may
|
||||
* be used to map a bare executable name to a full path.
|
||||
*
|
||||
* @param {string[]} [options.arguments]
|
||||
* A list of strings to pass as arguments to the process.
|
||||
*
|
||||
* @param {object} [options.environment]
|
||||
* An object containing a key and value for each environment variable
|
||||
* to pass to the process. Only the object's own, enumerable properties
|
||||
* are added to the environment.
|
||||
*
|
||||
* @param {boolean} [options.environmentAppend]
|
||||
* If true, append the environment variables passed in `environment` to
|
||||
* the existing set of environment variables. Otherwise, the values in
|
||||
* 'environment' constitute the entire set of environment variables
|
||||
* passed to the new process.
|
||||
*
|
||||
* @param {string} [options.stderr]
|
||||
* Defines how the process's stderr output is handled. One of:
|
||||
*
|
||||
* - `"ignore"`: (default) The process's standard error is not redirected.
|
||||
* - `"stdout"`: The process's stderr is merged with its stdout.
|
||||
* - `"pipe"`: The process's stderr is redirected to a pipe, which can be read
|
||||
* from via its `stderr` property.
|
||||
*
|
||||
* @param {string} [options.workdir]
|
||||
* The working directory in which to launch the new process.
|
||||
*
|
||||
* @returns {Promise<Process>}
|
||||
*
|
||||
* @rejects {Error}
|
||||
* May be rejected with an Error object if the process can not be
|
||||
* launched. The object will include an `errorCode` property with
|
||||
* one of the following values if it was rejected for the
|
||||
* corresponding reason:
|
||||
*
|
||||
* - Subprocess.ERROR_BAD_EXECUTABLE: The given command could not
|
||||
* be found, or the file that it references is not executable.
|
||||
*
|
||||
* Note that if the process is successfully launched, but exits with
|
||||
* a non-zero exit code, the promise will still resolve successfully.
|
||||
*/
|
||||
call(options) {
|
||||
options = Object.assign({}, options);
|
||||
|
||||
options.stderr = options.stderr || "ignore";
|
||||
options.workdir = options.workdir || null;
|
||||
|
||||
let environment = {};
|
||||
if (!options.environment || options.environmentAppend) {
|
||||
environment = this.getEnvironment();
|
||||
}
|
||||
|
||||
if (options.environment) {
|
||||
Object.assign(environment, options.environment);
|
||||
}
|
||||
|
||||
options.environment = Object.keys(environment)
|
||||
.map(key => `${key}=${environment[key]}`);
|
||||
|
||||
options.arguments = Array.from(options.arguments || []);
|
||||
|
||||
return Promise.resolve(SubprocessImpl.isExecutableFile(options.command)).then(isExecutable => {
|
||||
if (!isExecutable) {
|
||||
let error = new Error(`File at path "${options.command}" does not exist, or is not executable`);
|
||||
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
|
||||
throw error;
|
||||
}
|
||||
|
||||
options.arguments.unshift(options.command);
|
||||
|
||||
return SubprocessImpl.call(options);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an object with a key-value pair for every variable in the process's
|
||||
* current environment.
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
getEnvironment() {
|
||||
let environment = Object.create(null);
|
||||
for (let [k, v] of SubprocessImpl.getEnvironment()) {
|
||||
environment[k] = v;
|
||||
}
|
||||
return environment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Searches for the given executable file in the system executable
|
||||
* file paths as specified by the PATH environment variable.
|
||||
*
|
||||
* On Windows, if the unadorned filename cannot be found, the
|
||||
* extensions in the semicolon-separated list in the PATHSEP
|
||||
* environment variable are successively appended to the original
|
||||
* name and searched for in turn.
|
||||
*
|
||||
* @param {string} bin
|
||||
* The name of the executable to find.
|
||||
* @param {object} [environment]
|
||||
* An object containing a key for each environment variable to be used
|
||||
* in the search. If not provided, full the current process environment
|
||||
* is used.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
pathSearch(command, environment = this.getEnvironment()) {
|
||||
// Promise.resolve lets us get around returning one of the Promise.jsm
|
||||
// pseudo-promises returned by Task.jsm.
|
||||
let path = SubprocessImpl.pathSearch(command, environment);
|
||||
return Promise.resolve(path);
|
||||
},
|
||||
};
|
||||
|
||||
Object.assign(Subprocess, SubprocessConstants);
|
||||
Object.freeze(Subprocess);
|
|
@ -0,0 +1,227 @@
|
|||
.. _Subprocess:
|
||||
|
||||
=================
|
||||
Supbrocess Module
|
||||
=================
|
||||
|
||||
The Subprocess module allows a caller to spawn a native host executable, and
|
||||
communicate with it asynchronously over its standard input and output pipes.
|
||||
|
||||
Processes are launched asynchronously ``Subprocess.call`` method, based
|
||||
on the properties of a single options object. The method returns a promise
|
||||
which resolves, once the process has successfully launched, to a ``Process``
|
||||
object, which can be used to communicate with and control the process.
|
||||
|
||||
A simple Hello World invocation, which writes a message to a process, reads it
|
||||
back, logs it, and waits for the process to exit looks something like:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let proc = await Subprocess.call({
|
||||
command: "/bin/cat",
|
||||
});
|
||||
|
||||
proc.stdin.write("Hello World!");
|
||||
|
||||
let result = await proc.stdout.readString();
|
||||
console.log(result);
|
||||
|
||||
proc.stdin.close();
|
||||
let {exitCode} = await proc.wait();
|
||||
|
||||
Input and Output Redirection
|
||||
============================
|
||||
|
||||
Communication with the child process happens entirely via one-way pipes tied
|
||||
to its standard input, standard output, and standard error file descriptors.
|
||||
While standard input and output are always redirected to pipes, standard error
|
||||
is inherited from the parent process by default. Standard error can, however,
|
||||
optionally be either redirected to its own pipe or merged into the standard
|
||||
output pipe.
|
||||
|
||||
The module is designed primarily for use with processes following a strict
|
||||
IO protocol, with predictable message sizes. Its read operations, therefore,
|
||||
either complete after reading the exact amount of data specified, or do not
|
||||
complete at all. For cases where this is not desirable, ``read()`` and
|
||||
``readString`` may be called without any length argument, and will return a
|
||||
chunk of data of an arbitrary size.
|
||||
|
||||
|
||||
Process and Pipe Lifecycles
|
||||
===========================
|
||||
|
||||
Once the process exits, any buffered data from its output pipes may still be
|
||||
read until the pipe is explicitly closed. Unless the pipe is explicitly
|
||||
closed, however, any pending buffered data *must* be read from the pipe, or
|
||||
the resources associated with the pipe will not be freed.
|
||||
|
||||
Beyond this, no explicit cleanup is required for either processes or their
|
||||
pipes. So long as the caller ensures that the process exits, and there is no
|
||||
pending input to be read on its ``stdout`` or ``stderr`` pipes, all resources
|
||||
will be freed automatically.
|
||||
|
||||
The preferred way to ensure that a process exits is to close its input pipe
|
||||
and wait for it to exit gracefully. Processes which haven't exited gracefully
|
||||
by shutdown time, however, must be forcibly terminated:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let proc = await Subprocess.call({
|
||||
command: "/usr/bin/subprocess.py",
|
||||
});
|
||||
|
||||
// Kill the process if it hasn't gracefully exited by shutdown time.
|
||||
let blocker = () => proc.kill();
|
||||
|
||||
AsyncShutdown.profileBeforeChange.addBlocker(
|
||||
"Subprocess: Killing hung process",
|
||||
blocker);
|
||||
|
||||
proc.wait().then(() => {
|
||||
// Remove the shutdown blocker once we've exited.
|
||||
AsyncShutdown.profileBeforeChange.removeBlocker(blocker);
|
||||
|
||||
// Close standard output, in case there's any buffered data we haven't read.
|
||||
proc.stdout.close();
|
||||
});
|
||||
|
||||
// Send a message to the process, and close stdin, so the process knows to
|
||||
// exit.
|
||||
proc.stdin.write(message);
|
||||
proc.stdin.close();
|
||||
|
||||
In the simpler case of a short-running process which takes no input, and exits
|
||||
immediately after producing output, it's generally enough to simply read its
|
||||
output stream until EOF:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let proc = await Subprocess.call({
|
||||
command: await Subprocess.pathSearch("ifconfig"),
|
||||
});
|
||||
|
||||
// Read all of the process output.
|
||||
let result = "";
|
||||
let string;
|
||||
while ((string = await proc.stdout.readString())) {
|
||||
result += string;
|
||||
}
|
||||
console.log(result);
|
||||
|
||||
// The output pipe is closed and no buffered data remains to be read.
|
||||
// This means the process has exited, and no further cleanup is necessary.
|
||||
|
||||
|
||||
Bidirectional IO
|
||||
================
|
||||
|
||||
When performing bidirectional IO, special care needs to be taken to avoid
|
||||
deadlocks. While all IO operations in the Subprocess API are asynchronous,
|
||||
careless ordering of operations can still lead to a state where both processes
|
||||
are blocked on a read or write operation at the same time. For example,
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let proc = await Subprocess.call({
|
||||
command: "/bin/cat",
|
||||
});
|
||||
|
||||
let size = 1024 * 1024;
|
||||
await proc.stdin.write(new ArrayBuffer(size));
|
||||
|
||||
let result = await proc.stdout.read(size);
|
||||
|
||||
The code attempts to write 1MB of data to an input pipe, and then read it back
|
||||
from the output pipe. Because the data is big enough to fill both the input
|
||||
and output pipe buffers, though, and because the code waits for the write
|
||||
operation to complete before attempting any reads, the ``cat`` process will
|
||||
block trying to write to its output indefinitely, and never finish reading the
|
||||
data from its standard input.
|
||||
|
||||
In order to avoid the deadlock, we need to avoid blocking on the write
|
||||
operation:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let size = 1024 * 1024;
|
||||
proc.stdin.write(new ArrayBuffer(size));
|
||||
|
||||
let result = await proc.stdout.read(size);
|
||||
|
||||
There is no silver bullet to avoiding deadlocks in this type of situation,
|
||||
though. Any input operations that depend on output operations, or vice versa,
|
||||
have the possibility of triggering deadlocks, and need to be thought out
|
||||
carefully.
|
||||
|
||||
Arguments
|
||||
=========
|
||||
|
||||
Arguments may be passed to the process in the form an array of strings.
|
||||
Arguments are never split, or subjected to any sort of shell expansion, so the
|
||||
target process will receive the exact arguments array as passed to
|
||||
``Subprocess.call``. Argument 0 will always be the full path to the
|
||||
executable, as passed via the ``command`` argument:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let proc = await Subprocess.call({
|
||||
command: "/bin/sh",
|
||||
arguments: ["-c", "echo -n $0"],
|
||||
});
|
||||
|
||||
let output = await proc.stdout.readString();
|
||||
assert(output === "/bin/sh");
|
||||
|
||||
|
||||
Process Environment
|
||||
===================
|
||||
|
||||
By default, the process is launched with the same environment variables and
|
||||
working directory as the parent process, but either can be changed if
|
||||
necessary. The working directory may be changed simply by passing a
|
||||
``workdir`` option:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let proc = await Subprocess.call({
|
||||
command: "/bin/pwd",
|
||||
workdir: "/tmp",
|
||||
});
|
||||
|
||||
let output = await proc.stdout.readString();
|
||||
assert(output === "/tmp\n");
|
||||
|
||||
The process's environment variables can be changed using the ``environment``
|
||||
and ``environmentAppend`` options. By default, passing an ``environment``
|
||||
object replaces the process's entire environment with the properties in that
|
||||
object:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let proc = await Subprocess.call({
|
||||
command: "/bin/pwd",
|
||||
environment: {FOO: "BAR"},
|
||||
});
|
||||
|
||||
let output = await proc.stdout.readString();
|
||||
assert(output === "FOO=BAR\n");
|
||||
|
||||
In order to add variables to, or change variables from, the current set of
|
||||
environment variables, the ``environmentAppend`` object must be passed in
|
||||
addition:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let proc = await Subprocess.call({
|
||||
command: "/bin/pwd",
|
||||
environment: {FOO: "BAR"},
|
||||
environmentAppend: true,
|
||||
});
|
||||
|
||||
let output = "";
|
||||
while ((string = await proc.stdout.readString())) {
|
||||
output += string;
|
||||
}
|
||||
|
||||
assert(output.includes("FOO=BAR\n"));
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'Subprocess.jsm',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.subprocess += [
|
||||
'subprocess_common.jsm',
|
||||
'subprocess_shared.js',
|
||||
'subprocess_worker_common.js',
|
||||
]
|
||||
|
||||
if CONFIG['OS_TARGET'] == 'WINNT':
|
||||
EXTRA_JS_MODULES.subprocess += [
|
||||
'subprocess_shared_win.js',
|
||||
'subprocess_win.jsm',
|
||||
'subprocess_worker_win.js',
|
||||
]
|
||||
else:
|
||||
EXTRA_JS_MODULES.subprocess += [
|
||||
'subprocess_shared_unix.js',
|
||||
'subprocess_unix.jsm',
|
||||
'subprocess_worker_unix.js',
|
||||
]
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
|
||||
|
||||
SPHINX_TREES['toolkit_modules/subprocess'] = ['docs']
|
|
@ -0,0 +1,681 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* eslint-disable mozilla/balanced-listeners */
|
||||
|
||||
/* exported BaseProcess, PromiseWorker */
|
||||
|
||||
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.importGlobalProperties(["TextDecoder"]);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
|
||||
"resource://gre/modules/Timer.jsm");
|
||||
|
||||
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared.js", this);
|
||||
|
||||
var EXPORTED_SYMBOLS = ["BaseProcess", "PromiseWorker", "SubprocessConstants"];
|
||||
|
||||
const BUFFER_SIZE = 4096;
|
||||
|
||||
let nextResponseId = 0;
|
||||
|
||||
/**
|
||||
* Wraps a ChromeWorker so that messages sent to it return a promise which
|
||||
* resolves when the message has been received and the operation it triggers is
|
||||
* complete.
|
||||
*/
|
||||
class PromiseWorker extends ChromeWorker {
|
||||
constructor(url) {
|
||||
super(url);
|
||||
|
||||
this.listeners = new Map();
|
||||
this.pendingResponses = new Map();
|
||||
|
||||
this.addListener("failure", this.onFailure.bind(this));
|
||||
this.addListener("success", this.onSuccess.bind(this));
|
||||
this.addListener("debug", this.onDebug.bind(this));
|
||||
|
||||
this.addEventListener("message", this.onmessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for the given message from the worker. Any message received
|
||||
* from the worker with a `data.msg` property matching the given `msg`
|
||||
* parameter are passed to the given listener.
|
||||
*
|
||||
* @param {string} msg
|
||||
* The message to listen for.
|
||||
* @param {function(Event)} listener
|
||||
* The listener to call when matching messages are received.
|
||||
*/
|
||||
addListener(msg, listener) {
|
||||
if (!this.listeners.has(msg)) {
|
||||
this.listeners.set(msg, new Set());
|
||||
}
|
||||
this.listeners.get(msg).add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given message listener.
|
||||
*
|
||||
* @param {string} msg
|
||||
* The message to stop listening for.
|
||||
* @param {function(Event)} listener
|
||||
* The listener to remove.
|
||||
*/
|
||||
removeListener(msg, listener) {
|
||||
let listeners = this.listeners.get(msg);
|
||||
if (listeners) {
|
||||
listeners.delete(listener);
|
||||
|
||||
if (!listeners.size) {
|
||||
this.listeners.delete(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onmessage(event) {
|
||||
let {msg} = event.data;
|
||||
let listeners = this.listeners.get(msg) || new Set();
|
||||
|
||||
for (let listener of listeners) {
|
||||
try {
|
||||
listener(event.data);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a message sent to the worker has failed, and rejects its
|
||||
* corresponding promise.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
onFailure({msgId, error}) {
|
||||
this.pendingResponses.get(msgId).reject(error);
|
||||
this.pendingResponses.delete(msgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a message sent to the worker has succeeded, and resolves its
|
||||
* corresponding promise.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
onSuccess({msgId, data}) {
|
||||
this.pendingResponses.get(msgId).resolve(data);
|
||||
this.pendingResponses.delete(msgId);
|
||||
}
|
||||
|
||||
onDebug({message}) {
|
||||
dump(`Worker debug: ${message}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given method in the worker, and returns a promise which resolves
|
||||
* or rejects when the method has completed.
|
||||
*
|
||||
* @param {string} method
|
||||
* The name of the method to call.
|
||||
* @param {Array} args
|
||||
* The arguments to pass to the method.
|
||||
* @param {Array} [transferList]
|
||||
* A list of objects to transfer to the worker, rather than cloning.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
call(method, args, transferList = []) {
|
||||
let msgId = nextResponseId++;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pendingResponses.set(msgId, {resolve, reject});
|
||||
|
||||
let message = {
|
||||
msg: method,
|
||||
msgId,
|
||||
args,
|
||||
};
|
||||
|
||||
this.postMessage(message, transferList);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an input or output pipe connected to a subprocess.
|
||||
*
|
||||
* @property {integer} fd
|
||||
* The file descriptor number of the pipe on the child process's side.
|
||||
* @readonly
|
||||
*/
|
||||
class Pipe {
|
||||
/**
|
||||
* @param {Process} process
|
||||
* The child process that this pipe is connected to.
|
||||
* @param {integer} fd
|
||||
* The file descriptor number of the pipe on the child process's side.
|
||||
* @param {integer} id
|
||||
* The internal ID of the pipe, which ties it to the corresponding Pipe
|
||||
* object on the Worker side.
|
||||
*/
|
||||
constructor(process, fd, id) {
|
||||
this.id = id;
|
||||
this.fd = fd;
|
||||
this.processId = process.id;
|
||||
this.worker = process.worker;
|
||||
|
||||
/**
|
||||
* @property {boolean} closed
|
||||
* True if the file descriptor has been closed, and can no longer
|
||||
* be read from or written to. Pending IO operations may still
|
||||
* complete, but new operations may not be initiated.
|
||||
* @readonly
|
||||
*/
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the end of the pipe which belongs to this process.
|
||||
*
|
||||
* @param {boolean} force
|
||||
* If true, the pipe is closed immediately, regardless of any pending
|
||||
* IO operations. If false, the pipe is closed after any existing
|
||||
* pending IO operations have completed.
|
||||
* @returns {Promise<object>}
|
||||
* Resolves to an object with no properties once the pipe has been
|
||||
* closed.
|
||||
*/
|
||||
close(force = false) {
|
||||
this.closed = true;
|
||||
return this.worker.call("close", [this.id, force]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an output-only pipe, to which data may be written.
|
||||
*/
|
||||
class OutputPipe extends Pipe {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.encoder = new TextEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given data to the stream.
|
||||
*
|
||||
* When given an array buffer or typed array, ownership of the buffer is
|
||||
* transferred to the IO worker, and it may no longer be used from this
|
||||
* thread.
|
||||
*
|
||||
* @param {ArrayBuffer|TypedArray|string} buffer
|
||||
* Data to write to the stream.
|
||||
* @returns {Promise<object>}
|
||||
* Resolves to an object with a `bytesWritten` property, containing
|
||||
* the number of bytes successfully written, once the operation has
|
||||
* completed.
|
||||
*
|
||||
* @rejects {object}
|
||||
* May be rejected with an Error object, or an object with similar
|
||||
* properties. The object will include an `errorCode` property with
|
||||
* one of the following values if it was rejected for the
|
||||
* corresponding reason:
|
||||
*
|
||||
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
|
||||
* all of the data in `buffer` could be written to it.
|
||||
*/
|
||||
write(buffer) {
|
||||
if (typeof buffer === "string") {
|
||||
buffer = this.encoder.encode(buffer);
|
||||
}
|
||||
|
||||
if (Cu.getClassName(buffer, true) !== "ArrayBuffer") {
|
||||
if (buffer.byteLength === buffer.buffer.byteLength) {
|
||||
buffer = buffer.buffer;
|
||||
} else {
|
||||
buffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||
}
|
||||
}
|
||||
|
||||
let args = [this.id, buffer];
|
||||
|
||||
return this.worker.call("write", args, [buffer]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an input-only pipe, from which data may be read.
|
||||
*/
|
||||
class InputPipe extends Pipe {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.buffers = [];
|
||||
|
||||
/**
|
||||
* @property {integer} dataAvailable
|
||||
* The number of readable bytes currently stored in the input
|
||||
* buffer.
|
||||
* @readonly
|
||||
*/
|
||||
this.dataAvailable = 0;
|
||||
|
||||
this.decoder = new TextDecoder();
|
||||
|
||||
this.pendingReads = [];
|
||||
|
||||
this._pendingBufferRead = null;
|
||||
|
||||
this.fillBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {integer} bufferSize
|
||||
* The current size of the input buffer. This varies depending on
|
||||
* the size of pending read operations.
|
||||
* @readonly
|
||||
*/
|
||||
get bufferSize() {
|
||||
if (this.pendingReads.length) {
|
||||
return Math.max(this.pendingReads[0].length, BUFFER_SIZE);
|
||||
}
|
||||
return BUFFER_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to fill the input buffer.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
fillBuffer() {
|
||||
let dataWanted = this.bufferSize - this.dataAvailable;
|
||||
|
||||
if (!this._pendingBufferRead && dataWanted > 0) {
|
||||
this._pendingBufferRead = this._read(dataWanted);
|
||||
|
||||
this._pendingBufferRead.then((result) => {
|
||||
this._pendingBufferRead = null;
|
||||
|
||||
if (result) {
|
||||
this.onInput(result.buffer);
|
||||
|
||||
this.fillBuffer();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_read(size) {
|
||||
let args = [this.id, size];
|
||||
|
||||
return this.worker.call("read", args).catch(e => {
|
||||
this.closed = true;
|
||||
|
||||
for (let {length, resolve, reject} of this.pendingReads.splice(0)) {
|
||||
if (length === null && e.errorCode === SubprocessConstants.ERROR_END_OF_FILE) {
|
||||
resolve(new ArrayBuffer(0));
|
||||
} else {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given data to the end of the input buffer.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
onInput(buffer) {
|
||||
this.buffers.push(buffer);
|
||||
this.dataAvailable += buffer.byteLength;
|
||||
this.checkPendingReads();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the topmost pending read operations and fulfills as many as can be
|
||||
* filled from the current input buffer.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
checkPendingReads() {
|
||||
this.fillBuffer();
|
||||
|
||||
let reads = this.pendingReads;
|
||||
while (reads.length && this.dataAvailable &&
|
||||
reads[0].length <= this.dataAvailable) {
|
||||
let pending = this.pendingReads.shift();
|
||||
|
||||
let length = pending.length || this.dataAvailable;
|
||||
|
||||
let result;
|
||||
let byteLength = this.buffers[0].byteLength;
|
||||
if (byteLength == length) {
|
||||
result = this.buffers.shift();
|
||||
} else if (byteLength > length) {
|
||||
let buffer = this.buffers[0];
|
||||
|
||||
this.buffers[0] = buffer.slice(length);
|
||||
result = ArrayBuffer.transfer(buffer, length);
|
||||
} else {
|
||||
result = ArrayBuffer.transfer(this.buffers.shift(), length);
|
||||
let u8result = new Uint8Array(result);
|
||||
|
||||
while (byteLength < length) {
|
||||
let buffer = this.buffers[0];
|
||||
let u8buffer = new Uint8Array(buffer);
|
||||
|
||||
let remaining = length - byteLength;
|
||||
|
||||
if (buffer.byteLength <= remaining) {
|
||||
this.buffers.shift();
|
||||
|
||||
u8result.set(u8buffer, byteLength);
|
||||
} else {
|
||||
this.buffers[0] = buffer.slice(remaining);
|
||||
|
||||
u8result.set(u8buffer.subarray(0, remaining), byteLength);
|
||||
}
|
||||
|
||||
byteLength += Math.min(buffer.byteLength, remaining);
|
||||
}
|
||||
}
|
||||
|
||||
this.dataAvailable -= result.byteLength;
|
||||
pending.resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads exactly `length` bytes of binary data from the input stream, or, if
|
||||
* length is not provided, reads the first chunk of data to become available.
|
||||
* In the latter case, returns an empty array buffer on end of file.
|
||||
*
|
||||
* The read operation will not complete until enough data is available to
|
||||
* fulfill the request. If the pipe closes without enough available data to
|
||||
* fulfill the read, the operation fails, and any remaining buffered data is
|
||||
* lost.
|
||||
*
|
||||
* @param {integer} [length]
|
||||
* The number of bytes to read.
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
*
|
||||
* @rejects {object}
|
||||
* May be rejected with an Error object, or an object with similar
|
||||
* properties. The object will include an `errorCode` property with
|
||||
* one of the following values if it was rejected for the
|
||||
* corresponding reason:
|
||||
*
|
||||
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
|
||||
* enough input could be read to satisfy the request.
|
||||
*/
|
||||
read(length = null) {
|
||||
if (length !== null && !(Number.isInteger(length) && length >= 0)) {
|
||||
throw new RangeError("Length must be a non-negative integer");
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
return Promise.resolve(new ArrayBuffer(0));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pendingReads.push({length, resolve, reject});
|
||||
this.checkPendingReads();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads exactly `length` bytes from the input stream, and parses them as
|
||||
* UTF-8 JSON data.
|
||||
*
|
||||
* @param {integer} length
|
||||
* The number of bytes to read.
|
||||
* @returns {Promise<object>}
|
||||
*
|
||||
* @rejects {object}
|
||||
* May be rejected with an Error object, or an object with similar
|
||||
* properties. The object will include an `errorCode` property with
|
||||
* one of the following values if it was rejected for the
|
||||
* corresponding reason:
|
||||
*
|
||||
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
|
||||
* enough input could be read to satisfy the request.
|
||||
* - Subprocess.ERROR_INVALID_JSON: The data read from the pipe
|
||||
* could not be parsed as a valid JSON string.
|
||||
*/
|
||||
readJSON(length) {
|
||||
if (!Number.isInteger(length) || length <= 0) {
|
||||
throw new RangeError("Length must be a positive integer");
|
||||
}
|
||||
|
||||
return this.readString(length).then(string => {
|
||||
try {
|
||||
return JSON.parse(string);
|
||||
} catch (e) {
|
||||
e.errorCode = SubprocessConstants.ERROR_INVALID_JSON;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a chunk of UTF-8 data from the input stream, and converts it to a
|
||||
* JavaScript string.
|
||||
*
|
||||
* If `length` is provided, reads exactly `length` bytes. Otherwise, reads the
|
||||
* first chunk of data to become available, and returns an empty string on end
|
||||
* of file. In the latter case, the chunk is decoded in streaming mode, and
|
||||
* any incomplete UTF-8 sequences at the end of a chunk are returned at the
|
||||
* start of a subsequent read operation.
|
||||
*
|
||||
* @param {integer} [length]
|
||||
* The number of bytes to read.
|
||||
* @param {object} [options]
|
||||
* An options object as expected by TextDecoder.decode.
|
||||
* @returns {Promise<string>}
|
||||
*
|
||||
* @rejects {object}
|
||||
* May be rejected with an Error object, or an object with similar
|
||||
* properties. The object will include an `errorCode` property with
|
||||
* one of the following values if it was rejected for the
|
||||
* corresponding reason:
|
||||
*
|
||||
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
|
||||
* enough input could be read to satisfy the request.
|
||||
*/
|
||||
readString(length = null, options = {stream: length === null}) {
|
||||
if (length !== null && !(Number.isInteger(length) && length >= 0)) {
|
||||
throw new RangeError("Length must be a non-negative integer");
|
||||
}
|
||||
|
||||
return this.read(length).then(buffer => {
|
||||
return this.decoder.decode(buffer, options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads 4 bytes from the input stream, and parses them as an unsigned
|
||||
* integer, in native byte order.
|
||||
*
|
||||
* @returns {Promise<integer>}
|
||||
*
|
||||
* @rejects {object}
|
||||
* May be rejected with an Error object, or an object with similar
|
||||
* properties. The object will include an `errorCode` property with
|
||||
* one of the following values if it was rejected for the
|
||||
* corresponding reason:
|
||||
*
|
||||
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
|
||||
* enough input could be read to satisfy the request.
|
||||
*/
|
||||
readUint32() {
|
||||
return this.read(4).then(buffer => {
|
||||
return new Uint32Array(buffer)[0];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Process
|
||||
* @extends BaseProcess
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a currently-running process, and allows interaction with it.
|
||||
*/
|
||||
class BaseProcess {
|
||||
/**
|
||||
* @param {PromiseWorker} worker
|
||||
* The worker instance which owns the process.
|
||||
* @param {integer} processId
|
||||
* The internal ID of the Process object, which ties it to the
|
||||
* corresponding process on the Worker side.
|
||||
* @param {integer[]} fds
|
||||
* An array of internal Pipe IDs, one for each standard file descriptor
|
||||
* in the child process.
|
||||
* @param {integer} pid
|
||||
* The operating system process ID of the process.
|
||||
*/
|
||||
constructor(worker, processId, fds, pid) {
|
||||
this.id = processId;
|
||||
this.worker = worker;
|
||||
|
||||
/**
|
||||
* @property {integer} pid
|
||||
* The process ID of the process, assigned by the operating system.
|
||||
* @readonly
|
||||
*/
|
||||
this.pid = pid;
|
||||
|
||||
this.exitCode = null;
|
||||
|
||||
this.exitPromise = new Promise(resolve => {
|
||||
this.worker.call("wait", [this.id]).then(({exitCode}) => {
|
||||
resolve(Object.freeze({exitCode}));
|
||||
this.exitCode = exitCode;
|
||||
});
|
||||
});
|
||||
|
||||
if (fds[0] !== undefined) {
|
||||
/**
|
||||
* @property {OutputPipe} stdin
|
||||
* A Pipe object which allows writing to the process's standard
|
||||
* input.
|
||||
* @readonly
|
||||
*/
|
||||
this.stdin = new OutputPipe(this, 0, fds[0]);
|
||||
}
|
||||
if (fds[1] !== undefined) {
|
||||
/**
|
||||
* @property {InputPipe} stdout
|
||||
* A Pipe object which allows reading from the process's standard
|
||||
* output.
|
||||
* @readonly
|
||||
*/
|
||||
this.stdout = new InputPipe(this, 1, fds[1]);
|
||||
}
|
||||
if (fds[2] !== undefined) {
|
||||
/**
|
||||
* @property {InputPipe} [stderr]
|
||||
* An optional Pipe object which allows reading from the
|
||||
* process's standard error output.
|
||||
* @readonly
|
||||
*/
|
||||
this.stderr = new InputPipe(this, 2, fds[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a process, and resolves to a BaseProcess instance on success.
|
||||
*
|
||||
* @param {object} options
|
||||
* An options object as passed to `Subprocess.call`.
|
||||
*
|
||||
* @returns {Promise<BaseProcess>}
|
||||
*/
|
||||
static create(options) {
|
||||
let worker = this.getWorker();
|
||||
|
||||
return worker.call("spawn", [options]).then(({processId, fds, pid}) => {
|
||||
return new this(worker, processId, fds, pid);
|
||||
});
|
||||
}
|
||||
|
||||
static get WORKER_URL() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current subprocess worker, or spawns a new one if it does not
|
||||
* currently exist.
|
||||
*
|
||||
* @returns {PromiseWorker}
|
||||
*/
|
||||
static getWorker() {
|
||||
if (!this._worker) {
|
||||
this._worker = new PromiseWorker(this.WORKER_URL);
|
||||
}
|
||||
return this._worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills the process.
|
||||
*
|
||||
* @param {integer} [timeout=300]
|
||||
* A timeout, in milliseconds, after which the process will be forcibly
|
||||
* killed. On platforms which support it, the process will be sent
|
||||
* a `SIGTERM` signal immediately, so that it has a chance to terminate
|
||||
* gracefully, and a `SIGKILL` signal if it hasn't exited within
|
||||
* `timeout` milliseconds. On other platforms (namely Windows), the
|
||||
* process will be forcibly terminated immediately.
|
||||
*
|
||||
* @returns {Promise<object>}
|
||||
* Resolves to an object with an `exitCode` property when the process
|
||||
* has exited.
|
||||
*/
|
||||
kill(timeout = 300) {
|
||||
// If the process has already exited, don't bother sending a signal.
|
||||
if (this.exitCode != null) {
|
||||
return this.wait();
|
||||
}
|
||||
|
||||
let force = timeout <= 0;
|
||||
this.worker.call("kill", [this.id, force]);
|
||||
|
||||
if (!force) {
|
||||
setTimeout(() => {
|
||||
if (this.exitCode == null) {
|
||||
this.worker.call("kill", [this.id, true]);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
return this.wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise which resolves to the process's exit code, once it has
|
||||
* exited.
|
||||
*
|
||||
* @returns {Promise<object>}
|
||||
* Resolves to an object with an `exitCode` property, containing the
|
||||
* process's exit code, once the process has exited.
|
||||
*
|
||||
* On Unix-like systems, a negative exit code indicates that the
|
||||
* process was killed by a signal whose signal number is the absolute
|
||||
* value of the error code. On Windows, an exit code of -9 indicates
|
||||
* that the process was killed via the {@linkcode BaseProcess#kill kill()}
|
||||
* method.
|
||||
*/
|
||||
wait() {
|
||||
return this.exitPromise;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* exported Library, SubprocessConstants */
|
||||
|
||||
if (!ArrayBuffer.transfer) {
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer
|
||||
*/
|
||||
ArrayBuffer.transfer = function(buffer, size = buffer.byteLength) {
|
||||
let u8out = new Uint8Array(size);
|
||||
let u8buffer = new Uint8Array(buffer, 0, Math.min(size, buffer.byteLength));
|
||||
|
||||
u8out.set(u8buffer);
|
||||
|
||||
return u8out.buffer;
|
||||
};
|
||||
}
|
||||
|
||||
var libraries = {};
|
||||
|
||||
class Library {
|
||||
constructor(name, names, definitions) {
|
||||
if (name in libraries) {
|
||||
return libraries[name];
|
||||
}
|
||||
|
||||
for (let name of names) {
|
||||
try {
|
||||
if (!this.library) {
|
||||
this.library = ctypes.open(name);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors until we've tried all the options.
|
||||
}
|
||||
}
|
||||
if (!this.library) {
|
||||
throw new Error("Could not load libc");
|
||||
}
|
||||
|
||||
libraries[name] = this;
|
||||
|
||||
for (let symbol of Object.keys(definitions)) {
|
||||
this.declare(symbol, ...definitions[symbol]);
|
||||
}
|
||||
}
|
||||
|
||||
declare(name, ...args) {
|
||||
Object.defineProperty(this, name, {
|
||||
configurable: true,
|
||||
get() {
|
||||
Object.defineProperty(this, name, {
|
||||
configurable: true,
|
||||
value: this.library.declare(name, ...args),
|
||||
});
|
||||
|
||||
return this[name];
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds constants which apply to various Subprocess operations.
|
||||
* @namespace
|
||||
* @lends Subprocess
|
||||
*/
|
||||
const SubprocessConstants = {
|
||||
/**
|
||||
* @property {integer} ERROR_END_OF_FILE
|
||||
* The operation failed because the end of the file was reached.
|
||||
* @constant
|
||||
*/
|
||||
ERROR_END_OF_FILE: 0xff7a0001,
|
||||
/**
|
||||
* @property {integer} ERROR_INVALID_JSON
|
||||
* The operation failed because an invalid JSON was encountered.
|
||||
* @constant
|
||||
*/
|
||||
ERROR_INVALID_JSON: 0xff7a0002,
|
||||
/**
|
||||
* @property {integer} ERROR_BAD_EXECUTABLE
|
||||
* The operation failed because the given file did not exist, or
|
||||
* could not be executed.
|
||||
* @constant
|
||||
*/
|
||||
ERROR_BAD_EXECUTABLE: 0xff7a0003,
|
||||
};
|
||||
|
||||
Object.freeze(SubprocessConstants);
|
|
@ -0,0 +1,157 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* exported libc */
|
||||
|
||||
const LIBC = OS.Constants.libc;
|
||||
|
||||
const LIBC_CHOICES = ["libc.so", "libSystem.B.dylib", "a.out"];
|
||||
|
||||
const unix = {
|
||||
pid_t: ctypes.int32_t,
|
||||
|
||||
pollfd: new ctypes.StructType("pollfd", [
|
||||
{"fd": ctypes.int},
|
||||
{"events": ctypes.short},
|
||||
{"revents": ctypes.short},
|
||||
]),
|
||||
|
||||
posix_spawn_file_actions_t: ctypes.uint8_t.array(
|
||||
LIBC.OSFILE_SIZEOF_POSIX_SPAWN_FILE_ACTIONS_T),
|
||||
|
||||
WEXITSTATUS(status) {
|
||||
return (status >> 8) & 0xff;
|
||||
},
|
||||
|
||||
WTERMSIG(status) {
|
||||
return status & 0x7f;
|
||||
},
|
||||
};
|
||||
|
||||
var libc = new Library("libc", LIBC_CHOICES, {
|
||||
environ: [ctypes.char.ptr.ptr],
|
||||
|
||||
// Darwin-only.
|
||||
_NSGetEnviron: [
|
||||
ctypes.default_abi,
|
||||
ctypes.char.ptr.ptr.ptr,
|
||||
],
|
||||
|
||||
chdir: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
ctypes.char.ptr, /* path */
|
||||
],
|
||||
|
||||
close: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
ctypes.int, /* fildes */
|
||||
],
|
||||
|
||||
fcntl: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
ctypes.int, /* fildes */
|
||||
ctypes.int, /* cmd */
|
||||
ctypes.int, /* ... */
|
||||
],
|
||||
|
||||
getcwd: [
|
||||
ctypes.default_abi,
|
||||
ctypes.char.ptr,
|
||||
ctypes.char.ptr, /* buf */
|
||||
ctypes.size_t, /* size */
|
||||
],
|
||||
|
||||
kill: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
unix.pid_t, /* pid */
|
||||
ctypes.int, /* signal */
|
||||
],
|
||||
|
||||
pipe: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
ctypes.int.array(2), /* pipefd */
|
||||
],
|
||||
|
||||
poll: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
unix.pollfd.array(), /* fds */
|
||||
ctypes.unsigned_int, /* nfds */
|
||||
ctypes.int, /* timeout */
|
||||
],
|
||||
|
||||
posix_spawn: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
unix.pid_t.ptr, /* pid */
|
||||
ctypes.char.ptr, /* path */
|
||||
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
|
||||
ctypes.voidptr_t, /* attrp */
|
||||
ctypes.char.ptr.ptr, /* argv */
|
||||
ctypes.char.ptr.ptr, /* envp */
|
||||
],
|
||||
|
||||
posix_spawn_file_actions_addclose: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
|
||||
ctypes.int, /* fildes */
|
||||
],
|
||||
|
||||
posix_spawn_file_actions_adddup2: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
|
||||
ctypes.int, /* fildes */
|
||||
ctypes.int, /* newfildes */
|
||||
],
|
||||
|
||||
posix_spawn_file_actions_destroy: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
|
||||
],
|
||||
|
||||
posix_spawn_file_actions_init: [
|
||||
ctypes.default_abi,
|
||||
ctypes.int,
|
||||
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
|
||||
],
|
||||
|
||||
read: [
|
||||
ctypes.default_abi,
|
||||
ctypes.ssize_t,
|
||||
ctypes.int, /* fildes */
|
||||
ctypes.char.ptr, /* buf */
|
||||
ctypes.size_t, /* nbyte */
|
||||
],
|
||||
|
||||
waitpid: [
|
||||
ctypes.default_abi,
|
||||
unix.pid_t,
|
||||
unix.pid_t, /* pid */
|
||||
ctypes.int.ptr, /* status */
|
||||
ctypes.int, /* options */
|
||||
],
|
||||
|
||||
write: [
|
||||
ctypes.default_abi,
|
||||
ctypes.ssize_t,
|
||||
ctypes.int, /* fildes */
|
||||
ctypes.char.ptr, /* buf */
|
||||
ctypes.size_t, /* nbyte */
|
||||
],
|
||||
});
|
||||
|
||||
unix.Fd = function(fd) {
|
||||
return ctypes.CDataFinalizer(ctypes.int(fd), libc.close);
|
||||
};
|
|
@ -0,0 +1,343 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* exported LIBC, Win, createPipe, libc */
|
||||
|
||||
const LIBC = OS.Constants.libc;
|
||||
|
||||
const Win = OS.Constants.Win;
|
||||
|
||||
const LIBC_CHOICES = ["kernel32.dll"];
|
||||
|
||||
const win32 = {
|
||||
// On Windows 64, winapi_abi is an alias for default_abi.
|
||||
WINAPI: ctypes.winapi_abi,
|
||||
|
||||
BYTE: ctypes.uint8_t,
|
||||
WORD: ctypes.uint16_t,
|
||||
DWORD: ctypes.uint32_t,
|
||||
|
||||
UINT: ctypes.unsigned_int,
|
||||
UCHAR: ctypes.unsigned_char,
|
||||
|
||||
BOOL: ctypes.bool,
|
||||
|
||||
HANDLE: ctypes.voidptr_t,
|
||||
PVOID: ctypes.voidptr_t,
|
||||
LPVOID: ctypes.voidptr_t,
|
||||
|
||||
CHAR: ctypes.char,
|
||||
WCHAR: ctypes.jschar,
|
||||
|
||||
ULONG_PTR: ctypes.uintptr_t,
|
||||
};
|
||||
|
||||
Object.assign(win32, {
|
||||
LPSTR: win32.CHAR.ptr,
|
||||
LPWSTR: win32.WCHAR.ptr,
|
||||
|
||||
LPBYTE: win32.BYTE.ptr,
|
||||
LPDWORD: win32.DWORD.ptr,
|
||||
LPHANDLE: win32.HANDLE.ptr,
|
||||
});
|
||||
|
||||
Object.assign(win32, {
|
||||
LPCSTR: win32.LPSTR,
|
||||
LPCWSTR: win32.LPWSTR,
|
||||
LPCVOID: win32.LPVOID,
|
||||
});
|
||||
|
||||
Object.assign(win32, {
|
||||
CREATE_NEW_CONSOLE: 0x00000010,
|
||||
CREATE_UNICODE_ENVIRONMENT: 0x00000400,
|
||||
CREATE_NO_WINDOW: 0x08000000,
|
||||
|
||||
STARTF_USESTDHANDLES: 0x0100,
|
||||
|
||||
DUPLICATE_CLOSE_SOURCE: 0x01,
|
||||
DUPLICATE_SAME_ACCESS: 0x02,
|
||||
|
||||
ERROR_HANDLE_EOF: 38,
|
||||
ERROR_BROKEN_PIPE: 109,
|
||||
|
||||
FILE_FLAG_OVERLAPPED: 0x40000000,
|
||||
|
||||
PIPE_TYPE_BYTE: 0x00,
|
||||
|
||||
PIPE_ACCESS_INBOUND: 0x01,
|
||||
PIPE_ACCESS_OUTBOUND: 0x02,
|
||||
PIPE_ACCESS_DUPLEX: 0x03,
|
||||
|
||||
PIPE_WAIT: 0x00,
|
||||
PIPE_NOWAIT: 0x01,
|
||||
|
||||
STILL_ACTIVE: 259,
|
||||
|
||||
// These constants are 32-bit unsigned integers, but Windows defines
|
||||
// them as negative integers cast to an unsigned type.
|
||||
STD_INPUT_HANDLE: -10 + 0x100000000,
|
||||
STD_OUTPUT_HANDLE: -11 + 0x100000000,
|
||||
STD_ERROR_HANDLE: -12 + 0x100000000,
|
||||
|
||||
WAIT_TIMEOUT: 0x00000102,
|
||||
WAIT_FAILED: 0xffffffff,
|
||||
});
|
||||
|
||||
Object.assign(win32, {
|
||||
OVERLAPPED: new ctypes.StructType("OVERLAPPED", [
|
||||
{"Internal": win32.ULONG_PTR},
|
||||
{"InternalHigh": win32.ULONG_PTR},
|
||||
{"Offset": win32.DWORD},
|
||||
{"OffsetHigh": win32.DWORD},
|
||||
{"hEvent": win32.HANDLE},
|
||||
]),
|
||||
|
||||
PROCESS_INFORMATION: new ctypes.StructType("PROCESS_INFORMATION", [
|
||||
{"hProcess": win32.HANDLE},
|
||||
{"hThread": win32.HANDLE},
|
||||
{"dwProcessId": win32.DWORD},
|
||||
{"dwThreadId": win32.DWORD},
|
||||
]),
|
||||
|
||||
SECURITY_ATTRIBUTES: new ctypes.StructType("SECURITY_ATTRIBUTES", [
|
||||
{"nLength": win32.DWORD},
|
||||
{"lpSecurityDescriptor": win32.LPVOID},
|
||||
{"bInheritHandle": win32.BOOL},
|
||||
]),
|
||||
|
||||
STARTUPINFOW: new ctypes.StructType("STARTUPINFOW", [
|
||||
{"cb": win32.DWORD},
|
||||
{"lpReserved": win32.LPWSTR},
|
||||
{"lpDesktop": win32.LPWSTR},
|
||||
{"lpTitle": win32.LPWSTR},
|
||||
{"dwX": win32.DWORD},
|
||||
{"dwY": win32.DWORD},
|
||||
{"dwXSize": win32.DWORD},
|
||||
{"dwYSize": win32.DWORD},
|
||||
{"dwXCountChars": win32.DWORD},
|
||||
{"dwYCountChars": win32.DWORD},
|
||||
{"dwFillAttribute": win32.DWORD},
|
||||
{"dwFlags": win32.DWORD},
|
||||
{"wShowWindow": win32.WORD},
|
||||
{"cbReserved2": win32.WORD},
|
||||
{"lpReserved2": win32.LPBYTE},
|
||||
{"hStdInput": win32.HANDLE},
|
||||
{"hStdOutput": win32.HANDLE},
|
||||
{"hStdError": win32.HANDLE},
|
||||
]),
|
||||
});
|
||||
|
||||
var libc = new Library("libc", LIBC_CHOICES, {
|
||||
CloseHandle: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.HANDLE, /* hObject */
|
||||
],
|
||||
|
||||
CreateEventW: [
|
||||
win32.WINAPI,
|
||||
win32.HANDLE,
|
||||
win32.SECURITY_ATTRIBUTES.ptr, /* opt lpEventAttributes */
|
||||
win32.BOOL, /* bManualReset */
|
||||
win32.BOOL, /* bInitialState */
|
||||
win32.LPWSTR, /* lpName */
|
||||
],
|
||||
|
||||
CreateFileW: [
|
||||
win32.WINAPI,
|
||||
win32.HANDLE,
|
||||
win32.LPWSTR, /* lpFileName */
|
||||
win32.DWORD, /* dwDesiredAccess */
|
||||
win32.DWORD, /* dwShareMode */
|
||||
win32.SECURITY_ATTRIBUTES.ptr, /* opt lpSecurityAttributes */
|
||||
win32.DWORD, /* dwCreationDisposition */
|
||||
win32.DWORD, /* dwFlagsAndAttributes */
|
||||
win32.HANDLE, /* opt hTemplateFile */
|
||||
],
|
||||
|
||||
CreateNamedPipeW: [
|
||||
win32.WINAPI,
|
||||
win32.HANDLE,
|
||||
win32.LPWSTR, /* lpName */
|
||||
win32.DWORD, /* dwOpenMode */
|
||||
win32.DWORD, /* dwPipeMode */
|
||||
win32.DWORD, /* nMaxInstances */
|
||||
win32.DWORD, /* nOutBufferSize */
|
||||
win32.DWORD, /* nInBufferSize */
|
||||
win32.DWORD, /* nDefaultTimeOut */
|
||||
win32.SECURITY_ATTRIBUTES.ptr, /* opt lpSecurityAttributes */
|
||||
],
|
||||
|
||||
CreatePipe: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.LPHANDLE, /* out hReadPipe */
|
||||
win32.LPHANDLE, /* out hWritePipe */
|
||||
win32.SECURITY_ATTRIBUTES.ptr, /* opt lpPipeAttributes */
|
||||
win32.DWORD, /* nSize */
|
||||
],
|
||||
|
||||
CreateProcessW: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.LPCWSTR, /* lpApplicationName */
|
||||
win32.LPWSTR, /* lpCommandLine */
|
||||
win32.SECURITY_ATTRIBUTES.ptr, /* lpProcessAttributes */
|
||||
win32.SECURITY_ATTRIBUTES.ptr, /* lpThreadAttributes */
|
||||
win32.BOOL, /* bInheritHandle */
|
||||
win32.DWORD, /* dwCreationFlags */
|
||||
win32.LPVOID, /* opt lpEnvironment */
|
||||
win32.LPCWSTR, /* opt lpCurrentDirectory */
|
||||
win32.STARTUPINFOW.ptr, /* lpStartupInfo */
|
||||
win32.PROCESS_INFORMATION.ptr, /* out lpProcessInformation */
|
||||
],
|
||||
|
||||
DuplicateHandle: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.HANDLE, /* hSourceProcessHandle */
|
||||
win32.HANDLE, /* hSourceHandle */
|
||||
win32.HANDLE, /* hTargetProcessHandle */
|
||||
win32.LPHANDLE, /* out lpTargetHandle */
|
||||
win32.DWORD, /* dwDesiredAccess */
|
||||
win32.BOOL, /* bInheritHandle */
|
||||
win32.DWORD, /* dwOptions */
|
||||
],
|
||||
|
||||
FreeEnvironmentStringsW: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.LPCWSTR, /* lpszEnvironmentBlock */
|
||||
],
|
||||
|
||||
GetCurrentProcess: [
|
||||
win32.WINAPI,
|
||||
win32.HANDLE,
|
||||
],
|
||||
|
||||
GetCurrentProcessId: [
|
||||
win32.WINAPI,
|
||||
win32.DWORD,
|
||||
],
|
||||
|
||||
GetEnvironmentStringsW: [
|
||||
win32.WINAPI,
|
||||
win32.LPCWSTR,
|
||||
],
|
||||
|
||||
GetExitCodeProcess: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.HANDLE, /* hProcess */
|
||||
win32.LPDWORD, /* lpExitCode */
|
||||
],
|
||||
|
||||
GetOverlappedResult: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.HANDLE, /* hFile */
|
||||
win32.OVERLAPPED.ptr, /* lpOverlapped */
|
||||
win32.LPDWORD, /* lpNumberOfBytesTransferred */
|
||||
win32.BOOL, /* bWait */
|
||||
],
|
||||
|
||||
GetStdHandle: [
|
||||
win32.WINAPI,
|
||||
win32.HANDLE,
|
||||
win32.DWORD, /* nStdHandle */
|
||||
],
|
||||
|
||||
ReadFile: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.HANDLE, /* hFile */
|
||||
win32.LPVOID, /* out lpBuffer */
|
||||
win32.DWORD, /* nNumberOfBytesToRead */
|
||||
win32.LPDWORD, /* opt out lpNumberOfBytesRead */
|
||||
win32.OVERLAPPED.ptr, /* opt in/out lpOverlapped */
|
||||
],
|
||||
|
||||
TerminateProcess: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.HANDLE, /* hProcess */
|
||||
win32.UINT, /* uExitCode */
|
||||
],
|
||||
|
||||
WaitForMultipleObjects: [
|
||||
win32.WINAPI,
|
||||
win32.DWORD,
|
||||
win32.DWORD, /* nCount */
|
||||
win32.HANDLE.ptr, /* hHandles */
|
||||
win32.BOOL, /* bWaitAll */
|
||||
win32.DWORD, /* dwMilliseconds */
|
||||
],
|
||||
|
||||
WaitForSingleObject: [
|
||||
win32.WINAPI,
|
||||
win32.DWORD,
|
||||
win32.HANDLE, /* hHandle */
|
||||
win32.BOOL, /* bWaitAll */
|
||||
win32.DWORD, /* dwMilliseconds */
|
||||
],
|
||||
|
||||
WriteFile: [
|
||||
win32.WINAPI,
|
||||
win32.BOOL,
|
||||
win32.HANDLE, /* hFile */
|
||||
win32.LPCVOID, /* lpBuffer */
|
||||
win32.DWORD, /* nNumberOfBytesToRead */
|
||||
win32.LPDWORD, /* opt out lpNumberOfBytesWritten */
|
||||
win32.OVERLAPPED.ptr, /* opt in/out lpOverlapped */
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
let nextNamedPipeId = 0;
|
||||
|
||||
win32.Handle = function(handle) {
|
||||
return ctypes.CDataFinalizer(win32.HANDLE(handle), libc.CloseHandle);
|
||||
};
|
||||
|
||||
win32.createPipe = function(secAttr, readFlags = 0, writeFlags = 0, size = 0) {
|
||||
readFlags |= win32.PIPE_ACCESS_INBOUND;
|
||||
writeFlags |= Win.FILE_ATTRIBUTE_NORMAL;
|
||||
|
||||
if (size == 0) {
|
||||
size = 4096;
|
||||
}
|
||||
|
||||
let pid = libc.GetCurrentProcessId();
|
||||
let pipeName = String.raw`\\.\Pipe\SubProcessPipe.${pid}.${nextNamedPipeId++}`;
|
||||
|
||||
let readHandle = libc.CreateNamedPipeW(
|
||||
pipeName, readFlags,
|
||||
win32.PIPE_TYPE_BYTE | win32.PIPE_WAIT,
|
||||
1, /* number of connections */
|
||||
size, /* output buffer size */
|
||||
size, /* input buffer size */
|
||||
0, /* timeout */
|
||||
secAttr.address());
|
||||
|
||||
let isInvalid = handle => String(handle) == String(win32.HANDLE(Win.INVALID_HANDLE_VALUE));
|
||||
|
||||
if (isInvalid(readHandle)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let writeHandle = libc.CreateFileW(
|
||||
pipeName, Win.GENERIC_WRITE, 0, secAttr.address(),
|
||||
Win.OPEN_EXISTING, writeFlags, null);
|
||||
|
||||
if (isInvalid(writeHandle)) {
|
||||
libc.CloseHandle(readHandle);
|
||||
return [];
|
||||
}
|
||||
|
||||
return [win32.Handle(readHandle),
|
||||
win32.Handle(writeHandle)];
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* eslint-disable mozilla/balanced-listeners */
|
||||
|
||||
/* exported SubprocessImpl */
|
||||
|
||||
/* globals BaseProcess */
|
||||
|
||||
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
var EXPORTED_SYMBOLS = ["SubprocessImpl"];
|
||||
|
||||
Cu.import("resource://gre/modules/ctypes.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/subprocess/subprocess_common.jsm");
|
||||
|
||||
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared.js", this);
|
||||
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared_unix.js", this);
|
||||
|
||||
class Process extends BaseProcess {
|
||||
static get WORKER_URL() {
|
||||
return "resource://gre/modules/subprocess/subprocess_worker_unix.js";
|
||||
}
|
||||
}
|
||||
|
||||
var SubprocessUnix = {
|
||||
Process,
|
||||
|
||||
call(options) {
|
||||
return Process.create(options);
|
||||
},
|
||||
|
||||
* getEnvironment() {
|
||||
let environ;
|
||||
if (OS.Constants.Sys.Name == "Darwin") {
|
||||
environ = libc._NSGetEnviron().contents;
|
||||
} else {
|
||||
environ = libc.environ;
|
||||
}
|
||||
|
||||
for (let envp = environ; !envp.contents.isNull(); envp = envp.increment()) {
|
||||
let str = envp.contents.readString();
|
||||
|
||||
let idx = str.indexOf("=");
|
||||
if (idx >= 0) {
|
||||
yield [str.slice(0, idx),
|
||||
str.slice(idx + 1)];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isExecutableFile: Task.async(function* isExecutable(path) {
|
||||
if (!OS.Path.split(path).absolute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
let info = yield OS.File.stat(path);
|
||||
|
||||
// FIXME: We really want access(path, X_OK) here, but OS.File does not
|
||||
// support it.
|
||||
return !info.isDir && (info.unixMode & 0o111);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Searches for the given executable file in the system executable
|
||||
* file paths as specified by the PATH environment variable.
|
||||
*
|
||||
* On Windows, if the unadorned filename cannot be found, the
|
||||
* extensions in the semicolon-separated list in the PATHEXT
|
||||
* environment variable are successively appended to the original
|
||||
* name and searched for in turn.
|
||||
*
|
||||
* @param {string} bin
|
||||
* The name of the executable to find.
|
||||
* @param {object} environment
|
||||
* An object containing a key for each environment variable to be used
|
||||
* in the search.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
pathSearch: Task.async(function* (bin, environment) {
|
||||
let split = OS.Path.split(bin);
|
||||
if (split.absolute) {
|
||||
if (yield this.isExecutableFile(bin)) {
|
||||
return bin;
|
||||
}
|
||||
let error = new Error(`File at path "${bin}" does not exist, or is not executable`);
|
||||
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
|
||||
throw error;
|
||||
}
|
||||
|
||||
let dirs = [];
|
||||
if (environment.PATH) {
|
||||
dirs = environment.PATH.split(":");
|
||||
}
|
||||
|
||||
for (let dir of dirs) {
|
||||
let path = OS.Path.join(dir, bin);
|
||||
|
||||
if (yield this.isExecutableFile(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
let error = new Error(`Executable not found: ${bin}`);
|
||||
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
|
||||
throw error;
|
||||
}),
|
||||
};
|
||||
|
||||
var SubprocessImpl = SubprocessUnix;
|
|
@ -0,0 +1,138 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* eslint-disable mozilla/balanced-listeners */
|
||||
|
||||
/* exported SubprocessImpl */
|
||||
|
||||
/* globals BaseProcess */
|
||||
|
||||
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
var EXPORTED_SYMBOLS = ["SubprocessImpl"];
|
||||
|
||||
Cu.import("resource://gre/modules/ctypes.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/subprocess/subprocess_common.jsm");
|
||||
|
||||
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared.js", this);
|
||||
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared_win.js", this);
|
||||
|
||||
class Process extends BaseProcess {
|
||||
static get WORKER_URL() {
|
||||
return "resource://gre/modules/subprocess/subprocess_worker_win.js";
|
||||
}
|
||||
}
|
||||
|
||||
var SubprocessWin = {
|
||||
Process,
|
||||
|
||||
call(options) {
|
||||
return Process.create(options);
|
||||
},
|
||||
|
||||
|
||||
* getEnvironment() {
|
||||
let env = libc.GetEnvironmentStringsW();
|
||||
try {
|
||||
for (let p = env, q = env; ; p = p.increment()) {
|
||||
if (p.contents == "\0") {
|
||||
if (String(p) == String(q)) {
|
||||
break;
|
||||
}
|
||||
|
||||
let str = q.readString();
|
||||
q = p.increment();
|
||||
|
||||
let idx = str.indexOf("=");
|
||||
if (idx == 0) {
|
||||
idx = str.indexOf("=", 1);
|
||||
}
|
||||
|
||||
if (idx >= 0) {
|
||||
yield [str.slice(0, idx), str.slice(idx + 1)];
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
libc.FreeEnvironmentStringsW(env);
|
||||
}
|
||||
},
|
||||
|
||||
isExecutableFile: Task.async(function* (path) {
|
||||
if (!OS.Path.split(path).absolute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
let info = yield OS.File.stat(path);
|
||||
return !(info.isDir || info.isSymlink);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Searches for the given executable file in the system executable
|
||||
* file paths as specified by the PATH environment variable.
|
||||
*
|
||||
* On Windows, if the unadorned filename cannot be found, the
|
||||
* extensions in the semicolon-separated list in the PATHEXT
|
||||
* environment variable are successively appended to the original
|
||||
* name and searched for in turn.
|
||||
*
|
||||
* @param {string} bin
|
||||
* The name of the executable to find.
|
||||
* @param {object} environment
|
||||
* An object containing a key for each environment variable to be used
|
||||
* in the search.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
pathSearch: Task.async(function* (bin, environment) {
|
||||
let split = OS.Path.split(bin);
|
||||
if (split.absolute) {
|
||||
if (yield this.isExecutableFile(bin)) {
|
||||
return bin;
|
||||
}
|
||||
let error = new Error(`File at path "${bin}" does not exist, or is not a normal file`);
|
||||
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
|
||||
throw error;
|
||||
}
|
||||
|
||||
let dirs = [];
|
||||
let exts = [];
|
||||
if (environment.PATH) {
|
||||
dirs = environment.PATH.split(";");
|
||||
}
|
||||
if (environment.PATHEXT) {
|
||||
exts = environment.PATHEXT.split(";");
|
||||
}
|
||||
|
||||
for (let dir of dirs) {
|
||||
let path = OS.Path.join(dir, bin);
|
||||
|
||||
if (yield this.isExecutableFile(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
for (let ext of exts) {
|
||||
let file = path + ext;
|
||||
|
||||
if (yield this.isExecutableFile(file)) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
let error = new Error(`Executable not found: ${bin}`);
|
||||
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
|
||||
throw error;
|
||||
}),
|
||||
};
|
||||
|
||||
var SubprocessImpl = SubprocessWin;
|
|
@ -0,0 +1,193 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* exported BasePipe, BaseProcess, debug */
|
||||
/* globals Process, io */
|
||||
|
||||
function debug(message) {
|
||||
self.postMessage({msg: "debug", message});
|
||||
}
|
||||
|
||||
class BasePipe {
|
||||
constructor() {
|
||||
this.closing = false;
|
||||
this.closed = false;
|
||||
|
||||
this.closedPromise = new Promise(resolve => {
|
||||
this.resolveClosed = resolve;
|
||||
});
|
||||
|
||||
this.pending = [];
|
||||
}
|
||||
|
||||
shiftPending() {
|
||||
let result = this.pending.shift();
|
||||
|
||||
if (this.closing && this.pending.length == 0) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
let nextProcessId = 0;
|
||||
|
||||
class BaseProcess {
|
||||
constructor(options) {
|
||||
this.id = nextProcessId++;
|
||||
|
||||
this.exitCode = null;
|
||||
|
||||
this.exitPromise = new Promise(resolve => {
|
||||
this.resolveExit = resolve;
|
||||
});
|
||||
this.exitPromise.then(() => {
|
||||
// The input file descriptors will be closed after poll
|
||||
// reports that their input buffers are empty. If we close
|
||||
// them now, we may lose output.
|
||||
this.pipes[0].close(true);
|
||||
});
|
||||
|
||||
this.pid = null;
|
||||
this.pipes = [];
|
||||
|
||||
this.stringArrays = [];
|
||||
|
||||
this.spawn(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a null-terminated array of pointers to null-terminated C-strings,
|
||||
* and returns it.
|
||||
*
|
||||
* @param {string[]} strings
|
||||
* The strings to convert into a C string array.
|
||||
*
|
||||
* @returns {ctypes.char.ptr.array()}
|
||||
*/
|
||||
stringArray(strings) {
|
||||
let result = ctypes.char.ptr.array(strings.length + 1)();
|
||||
|
||||
let cstrings = strings.map(str => ctypes.char.array()(str));
|
||||
for (let [i, cstring] of cstrings.entries()) {
|
||||
result[i] = cstring;
|
||||
}
|
||||
|
||||
// Char arrays used in char arg and environment vectors must be
|
||||
// explicitly kept alive in a JS object, or they will be reaped
|
||||
// by the GC if it runs before our process is started.
|
||||
this.stringArrays.push(cstrings);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
let requests = {
|
||||
close(pipeId, force = false) {
|
||||
let pipe = io.getPipe(pipeId);
|
||||
|
||||
return pipe.close(force).then(() => ({data: {}}));
|
||||
},
|
||||
|
||||
spawn(options) {
|
||||
let process = new Process(options);
|
||||
let processId = process.id;
|
||||
|
||||
io.addProcess(process);
|
||||
|
||||
let fds = process.pipes.map(pipe => pipe.id);
|
||||
|
||||
return {data: {processId, fds, pid: process.pid}};
|
||||
},
|
||||
|
||||
kill(processId, force = false) {
|
||||
let process = io.getProcess(processId);
|
||||
|
||||
process.kill(force ? 9 : 15);
|
||||
|
||||
return {data: {}};
|
||||
},
|
||||
|
||||
wait(processId) {
|
||||
let process = io.getProcess(processId);
|
||||
|
||||
process.wait();
|
||||
|
||||
return process.exitPromise.then(exitCode => {
|
||||
io.cleanupProcess(process);
|
||||
return {data: {exitCode}};
|
||||
});
|
||||
},
|
||||
|
||||
read(pipeId, count) {
|
||||
let pipe = io.getPipe(pipeId);
|
||||
|
||||
return pipe.read(count).then(buffer => {
|
||||
return {data: {buffer}};
|
||||
});
|
||||
},
|
||||
|
||||
write(pipeId, buffer) {
|
||||
let pipe = io.getPipe(pipeId);
|
||||
|
||||
return pipe.write(buffer).then(bytesWritten => {
|
||||
return {data: {bytesWritten}};
|
||||
});
|
||||
},
|
||||
|
||||
getOpenFiles() {
|
||||
return {data: new Set(io.pipes.keys())};
|
||||
},
|
||||
|
||||
getProcesses() {
|
||||
let data = new Map(Array.from(io.processes.values(),
|
||||
proc => [proc.id, proc.pid]));
|
||||
return {data};
|
||||
},
|
||||
};
|
||||
|
||||
onmessage = event => {
|
||||
let {msg, msgId, args} = event.data;
|
||||
|
||||
new Promise(resolve => {
|
||||
resolve(requests[msg](...args));
|
||||
}).then(result => {
|
||||
let response = {
|
||||
msg: "success",
|
||||
msgId,
|
||||
data: result.data,
|
||||
};
|
||||
|
||||
self.postMessage(response, result.transfer || []);
|
||||
}).catch(error => {
|
||||
if (error instanceof Error) {
|
||||
error = {
|
||||
message: error.message,
|
||||
fileName: error.fileName,
|
||||
lineNumber: error.lineNumber,
|
||||
column: error.column,
|
||||
stack: error.stack,
|
||||
errorCode: error.errorCode,
|
||||
};
|
||||
}
|
||||
|
||||
self.postMessage({
|
||||
msg: "failure",
|
||||
msgId,
|
||||
error,
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
|
||||
self.postMessage({
|
||||
msg: "failure",
|
||||
msgId,
|
||||
error: {},
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,539 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* exported Process */
|
||||
/* globals BaseProcess, BasePipe */
|
||||
|
||||
importScripts("resource://gre/modules/subprocess/subprocess_shared.js",
|
||||
"resource://gre/modules/subprocess/subprocess_shared_unix.js",
|
||||
"resource://gre/modules/subprocess/subprocess_worker_common.js");
|
||||
|
||||
const POLL_INTERVAL = 50;
|
||||
const POLL_TIMEOUT = 0;
|
||||
|
||||
let io;
|
||||
|
||||
let nextPipeId = 0;
|
||||
|
||||
class Pipe extends BasePipe {
|
||||
constructor(process, fd) {
|
||||
super();
|
||||
|
||||
this.process = process;
|
||||
this.fd = fd;
|
||||
this.id = nextPipeId++;
|
||||
}
|
||||
|
||||
get pollEvents() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the file descriptor.
|
||||
*
|
||||
* @param {boolean} [force=false]
|
||||
* If true, the file descriptor is closed immediately. If false, the
|
||||
* file descriptor is closed after all current pending IO operations
|
||||
* have completed.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
* Resolves when the file descriptor has been closed.
|
||||
*/
|
||||
close(force = false) {
|
||||
if (!force && this.pending.length) {
|
||||
this.closing = true;
|
||||
return this.closedPromise;
|
||||
}
|
||||
|
||||
for (let {reject} of this.pending) {
|
||||
let error = new Error("File closed");
|
||||
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
|
||||
reject(error);
|
||||
}
|
||||
this.pending.length = 0;
|
||||
|
||||
if (!this.closed) {
|
||||
this.fd.dispose();
|
||||
|
||||
this.closed = true;
|
||||
this.resolveClosed();
|
||||
|
||||
io.pipes.delete(this.id);
|
||||
io.updatePollFds();
|
||||
}
|
||||
return this.closedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an error occurred while polling our file descriptor.
|
||||
*/
|
||||
onError() {
|
||||
this.close(true);
|
||||
this.process.wait();
|
||||
}
|
||||
}
|
||||
|
||||
class InputPipe extends Pipe {
|
||||
/**
|
||||
* A bit mask of poll() events which we currently wish to be notified of on
|
||||
* this file descriptor.
|
||||
*/
|
||||
get pollEvents() {
|
||||
if (this.pending.length) {
|
||||
return LIBC.POLLIN;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously reads at most `length` bytes of binary data from the file
|
||||
* descriptor into an ArrayBuffer of the same size. Returns a promise which
|
||||
* resolves when the operation is complete.
|
||||
*
|
||||
* @param {integer} length
|
||||
* The number of bytes to read.
|
||||
*
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
*/
|
||||
read(length) {
|
||||
if (this.closing || this.closed) {
|
||||
throw new Error("Attempt to read from closed pipe");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.push({resolve, reject, length});
|
||||
io.updatePollFds();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously reads at most `count` bytes of binary data into an
|
||||
* ArrayBuffer, and returns that buffer. If no data can be read without
|
||||
* blocking, returns null instead.
|
||||
*
|
||||
* @param {integer} count
|
||||
* The number of bytes to read.
|
||||
*
|
||||
* @returns {ArrayBuffer|null}
|
||||
*/
|
||||
readBuffer(count) {
|
||||
let buffer = new ArrayBuffer(count);
|
||||
|
||||
let read = +libc.read(this.fd, buffer, buffer.byteLength);
|
||||
if (read < 0 && ctypes.errno != LIBC.EAGAIN) {
|
||||
this.onError();
|
||||
}
|
||||
|
||||
if (read <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (read < buffer.byteLength) {
|
||||
return ArrayBuffer.transfer(buffer, read);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when one of the IO operations matching the `pollEvents` mask may be
|
||||
* performed without blocking.
|
||||
*/
|
||||
onReady() {
|
||||
let reads = this.pending;
|
||||
while (reads.length) {
|
||||
let {resolve, length} = reads[0];
|
||||
|
||||
let buffer = this.readBuffer(length);
|
||||
if (buffer) {
|
||||
this.shiftPending();
|
||||
resolve(buffer);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reads.length == 0) {
|
||||
io.updatePollFds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OutputPipe extends Pipe {
|
||||
/**
|
||||
* A bit mask of poll() events which we currently wish to be notified of on
|
||||
* this file discriptor.
|
||||
*/
|
||||
get pollEvents() {
|
||||
if (this.pending.length) {
|
||||
return LIBC.POLLOUT;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously writes the given buffer to our file descriptor, and returns
|
||||
* a promise which resolves when the operation is complete.
|
||||
*
|
||||
* @param {ArrayBuffer} buffer
|
||||
* The buffer to write.
|
||||
*
|
||||
* @returns {Promise<integer>}
|
||||
* Resolves to the number of bytes written when the operation is
|
||||
* complete.
|
||||
*/
|
||||
write(buffer) {
|
||||
if (this.closing || this.closed) {
|
||||
throw new Error("Attempt to write to closed pipe");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.push({resolve, reject, buffer, length: buffer.byteLength});
|
||||
io.updatePollFds();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to synchronously write the given buffer to our file descriptor.
|
||||
* Writes only as many bytes as can be written without blocking, and returns
|
||||
* the number of byes successfully written.
|
||||
*
|
||||
* Closes the file descriptor if an IO error occurs.
|
||||
*
|
||||
* @param {ArrayBuffer} buffer
|
||||
* The buffer to write.
|
||||
*
|
||||
* @returns {integer}
|
||||
* The number of bytes successfully written.
|
||||
*/
|
||||
writeBuffer(buffer) {
|
||||
let bytesWritten = libc.write(this.fd, buffer, buffer.byteLength);
|
||||
|
||||
if (bytesWritten < 0 && ctypes.errno != LIBC.EAGAIN) {
|
||||
this.onError();
|
||||
}
|
||||
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the IO operations matching the `pollEvents` mask may be
|
||||
* performed without blocking.
|
||||
*/
|
||||
onReady() {
|
||||
let writes = this.pending;
|
||||
while (writes.length) {
|
||||
let {buffer, resolve, length} = writes[0];
|
||||
|
||||
let written = this.writeBuffer(buffer);
|
||||
|
||||
if (written == buffer.byteLength) {
|
||||
resolve(length);
|
||||
this.shiftPending();
|
||||
} else if (written > 0) {
|
||||
writes[0].buffer = buffer.slice(written);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (writes.length == 0) {
|
||||
io.updatePollFds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Process extends BaseProcess {
|
||||
/**
|
||||
* Each Process object opens an additional pipe from the target object, which
|
||||
* will be automatically closed when the process exits, but otherwise
|
||||
* carries no data.
|
||||
*
|
||||
* This property contains a bit mask of poll() events which we wish to be
|
||||
* notified of on this descriptor. We're not expecting any input from this
|
||||
* pipe, but we need to poll for input until the process exits in order to be
|
||||
* notified when the pipe closes.
|
||||
*/
|
||||
get pollEvents() {
|
||||
if (this.exitCode === null) {
|
||||
return LIBC.POLLIN;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills the process with the given signal.
|
||||
*
|
||||
* @param {integer} signal
|
||||
*/
|
||||
kill(signal) {
|
||||
libc.kill(this.pid, signal);
|
||||
this.wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the IO pipes for use as standard input, output, and error
|
||||
* descriptors in the spawned process.
|
||||
*
|
||||
* @returns {unix.Fd[]}
|
||||
* The array of file descriptors belonging to the spawned process.
|
||||
*/
|
||||
initPipes(options) {
|
||||
let stderr = options.stderr;
|
||||
|
||||
let our_pipes = [];
|
||||
let their_pipes = new Map();
|
||||
|
||||
let pipe = input => {
|
||||
let fds = ctypes.int.array(2)();
|
||||
|
||||
let res = libc.pipe(fds);
|
||||
if (res == -1) {
|
||||
throw new Error("Unable to create pipe");
|
||||
}
|
||||
|
||||
fds = Array.from(fds, unix.Fd);
|
||||
|
||||
if (input) {
|
||||
fds.reverse();
|
||||
}
|
||||
|
||||
if (input) {
|
||||
our_pipes.push(new InputPipe(this, fds[1]));
|
||||
} else {
|
||||
our_pipes.push(new OutputPipe(this, fds[1]));
|
||||
}
|
||||
|
||||
libc.fcntl(fds[0], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
|
||||
libc.fcntl(fds[1], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
|
||||
libc.fcntl(fds[1], LIBC.F_SETFL, LIBC.O_NONBLOCK);
|
||||
|
||||
return fds[0];
|
||||
};
|
||||
|
||||
their_pipes.set(0, pipe(false));
|
||||
their_pipes.set(1, pipe(true));
|
||||
|
||||
if (stderr == "pipe") {
|
||||
their_pipes.set(2, pipe(true));
|
||||
} else if (stderr == "stdout") {
|
||||
their_pipes.set(2, their_pipes.get(1));
|
||||
}
|
||||
|
||||
// Create an additional pipe that we can use to monitor for process exit.
|
||||
their_pipes.set(3, pipe(true));
|
||||
this.fd = our_pipes.pop().fd;
|
||||
|
||||
this.pipes = our_pipes;
|
||||
|
||||
return their_pipes;
|
||||
}
|
||||
|
||||
spawn(options) {
|
||||
let {command, arguments: args} = options;
|
||||
|
||||
let argv = this.stringArray(args);
|
||||
let envp = this.stringArray(options.environment);
|
||||
|
||||
let actions = unix.posix_spawn_file_actions_t();
|
||||
let actionsp = actions.address();
|
||||
|
||||
let fds = this.initPipes(options);
|
||||
|
||||
let cwd;
|
||||
try {
|
||||
if (options.workdir) {
|
||||
cwd = ctypes.char.array(LIBC.PATH_MAX)();
|
||||
libc.getcwd(cwd, cwd.length);
|
||||
|
||||
if (libc.chdir(options.workdir) < 0) {
|
||||
throw new Error(`Unable to change working directory to ${options.workdir}`);
|
||||
}
|
||||
}
|
||||
|
||||
libc.posix_spawn_file_actions_init(actionsp);
|
||||
for (let [i, fd] of fds.entries()) {
|
||||
libc.posix_spawn_file_actions_adddup2(actionsp, fd, i);
|
||||
}
|
||||
|
||||
let pid = unix.pid_t();
|
||||
let rv = libc.posix_spawn(pid.address(), command, actionsp, null, argv, envp);
|
||||
|
||||
if (rv != 0) {
|
||||
for (let pipe of this.pipes) {
|
||||
pipe.close();
|
||||
}
|
||||
throw new Error(`Failed to execute command "${command}"`);
|
||||
}
|
||||
|
||||
this.pid = pid.value;
|
||||
} finally {
|
||||
libc.posix_spawn_file_actions_destroy(actionsp);
|
||||
|
||||
this.stringArrays.length = 0;
|
||||
|
||||
if (cwd) {
|
||||
libc.chdir(cwd);
|
||||
}
|
||||
for (let fd of new Set(fds.values())) {
|
||||
fd.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when input is available on our sentinel file descriptor.
|
||||
*
|
||||
* @see pollEvents
|
||||
*/
|
||||
onReady() {
|
||||
// We're not actually expecting any input on this pipe. If we get any, we
|
||||
// can't poll the pipe any further without reading it.
|
||||
if (this.wait() == undefined) {
|
||||
this.kill(9);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an error occurred while polling our sentinel file descriptor.
|
||||
*
|
||||
* @see pollEvents
|
||||
*/
|
||||
onError() {
|
||||
this.wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to wait for the process's exit status, without blocking. If
|
||||
* successful, resolves the `exitPromise` to the process's exit value.
|
||||
*
|
||||
* @returns {integer|null}
|
||||
* The process's exit status, if it has already exited.
|
||||
*/
|
||||
wait() {
|
||||
if (this.exitCode !== null) {
|
||||
return this.exitCode;
|
||||
}
|
||||
|
||||
let status = ctypes.int();
|
||||
|
||||
let res = libc.waitpid(this.pid, status.address(), LIBC.WNOHANG);
|
||||
if (res == this.pid) {
|
||||
let sig = unix.WTERMSIG(status.value);
|
||||
if (sig) {
|
||||
this.exitCode = -sig;
|
||||
} else {
|
||||
this.exitCode = unix.WEXITSTATUS(status.value);
|
||||
}
|
||||
|
||||
this.fd.dispose();
|
||||
this.resolveExit(this.exitCode);
|
||||
return this.exitCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
io = {
|
||||
pollFds: null,
|
||||
pollHandlers: null,
|
||||
|
||||
pipes: new Map(),
|
||||
|
||||
processes: new Map(),
|
||||
|
||||
interval: null,
|
||||
|
||||
getPipe(pipeId) {
|
||||
let pipe = this.pipes.get(pipeId);
|
||||
|
||||
if (!pipe) {
|
||||
let error = new Error("File closed");
|
||||
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
|
||||
throw error;
|
||||
}
|
||||
return pipe;
|
||||
},
|
||||
|
||||
getProcess(processId) {
|
||||
let process = this.processes.get(processId);
|
||||
|
||||
if (!process) {
|
||||
throw new Error(`Invalid process ID: ${processId}`);
|
||||
}
|
||||
return process;
|
||||
},
|
||||
|
||||
updatePollFds() {
|
||||
let handlers = [...this.pipes.values(),
|
||||
...this.processes.values()];
|
||||
|
||||
handlers = handlers.filter(handler => handler.pollEvents);
|
||||
|
||||
let pollfds = unix.pollfd.array(handlers.length)();
|
||||
|
||||
for (let [i, handler] of handlers.entries()) {
|
||||
let pollfd = pollfds[i];
|
||||
|
||||
pollfd.fd = handler.fd;
|
||||
pollfd.events = handler.pollEvents;
|
||||
pollfd.revents = 0;
|
||||
}
|
||||
|
||||
this.pollFds = pollfds;
|
||||
this.pollHandlers = handlers;
|
||||
|
||||
if (pollfds.length && !this.interval) {
|
||||
this.interval = setInterval(this.poll.bind(this), POLL_INTERVAL);
|
||||
} else if (!pollfds.length && this.interval) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
},
|
||||
|
||||
poll() {
|
||||
let handlers = this.pollHandlers;
|
||||
let pollfds = this.pollFds;
|
||||
|
||||
let count = libc.poll(pollfds, pollfds.length, POLL_TIMEOUT);
|
||||
|
||||
for (let i = 0; count && i < pollfds.length; i++) {
|
||||
let pollfd = pollfds[i];
|
||||
if (pollfd.revents) {
|
||||
count--;
|
||||
|
||||
let handler = handlers[i];
|
||||
try {
|
||||
if (pollfd.revents & handler.pollEvents) {
|
||||
handler.onReady();
|
||||
}
|
||||
if (pollfd.revents & (LIBC.POLLERR | LIBC.POLLHUP | LIBC.POLLNVAL)) {
|
||||
handler.onError();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
debug(`Worker error: ${e} :: ${e.stack}`);
|
||||
handler.onError();
|
||||
}
|
||||
|
||||
pollfd.revents = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addProcess(process) {
|
||||
this.processes.set(process.id, process);
|
||||
|
||||
for (let pipe of process.pipes) {
|
||||
this.pipes.set(pipe.id, pipe);
|
||||
}
|
||||
},
|
||||
|
||||
cleanupProcess(process) {
|
||||
this.processes.delete(process.id);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,594 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/* exported Process */
|
||||
/* globals BaseProcess, BasePipe, win32 */
|
||||
|
||||
importScripts("resource://gre/modules/subprocess/subprocess_shared.js",
|
||||
"resource://gre/modules/subprocess/subprocess_shared_win.js",
|
||||
"resource://gre/modules/subprocess/subprocess_worker_common.js");
|
||||
|
||||
const POLL_INTERVAL = 50;
|
||||
const POLL_TIMEOUT = 0;
|
||||
|
||||
// The exit code that we send when we forcibly terminate a process.
|
||||
const TERMINATE_EXIT_CODE = 0x7f;
|
||||
|
||||
let io;
|
||||
|
||||
let nextPipeId = 0;
|
||||
|
||||
class Pipe extends BasePipe {
|
||||
constructor(process, origHandle) {
|
||||
super();
|
||||
|
||||
let handle = win32.HANDLE();
|
||||
|
||||
let curProc = libc.GetCurrentProcess();
|
||||
libc.DuplicateHandle(curProc, origHandle, curProc, handle.address(),
|
||||
0, false /* inheritable */, win32.DUPLICATE_SAME_ACCESS);
|
||||
|
||||
origHandle.dispose();
|
||||
|
||||
this.id = nextPipeId++;
|
||||
this.process = process;
|
||||
|
||||
this.handle = win32.Handle(handle);
|
||||
|
||||
let event = libc.CreateEventW(null, false, false, null);
|
||||
|
||||
this.overlapped = win32.OVERLAPPED();
|
||||
this.overlapped.hEvent = event;
|
||||
|
||||
this._event = win32.Handle(event);
|
||||
|
||||
this.buffer = null;
|
||||
}
|
||||
|
||||
get event() {
|
||||
if (this.pending.length) {
|
||||
return this._event;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
maybeClose() {}
|
||||
|
||||
/**
|
||||
* Closes the file handle.
|
||||
*
|
||||
* @param {boolean} [force=false]
|
||||
* If true, the file handle is closed immediately. If false, the
|
||||
* file handle is closed after all current pending IO operations
|
||||
* have completed.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
* Resolves when the file handle has been closed.
|
||||
*/
|
||||
close(force = false) {
|
||||
if (!force && this.pending.length) {
|
||||
this.closing = true;
|
||||
return this.closedPromise;
|
||||
}
|
||||
|
||||
for (let {reject} of this.pending) {
|
||||
let error = new Error("File closed");
|
||||
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
|
||||
reject(error);
|
||||
}
|
||||
this.pending.length = 0;
|
||||
|
||||
this.buffer = null;
|
||||
|
||||
if (!this.closed) {
|
||||
this.handle.dispose();
|
||||
this._event.dispose();
|
||||
|
||||
io.pipes.delete(this.id);
|
||||
|
||||
this.handle = null;
|
||||
this.closed = true;
|
||||
this.resolveClosed();
|
||||
|
||||
io.updatePollEvents();
|
||||
}
|
||||
return this.closedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an error occurred while attempting an IO operation on our file
|
||||
* handle.
|
||||
*/
|
||||
onError() {
|
||||
this.close(true);
|
||||
}
|
||||
}
|
||||
|
||||
class InputPipe extends Pipe {
|
||||
/**
|
||||
* Queues the next chunk of data to be read from the pipe if, and only if,
|
||||
* there is no IO operation currently pending.
|
||||
*/
|
||||
readNext() {
|
||||
if (this.buffer === null) {
|
||||
this.readBuffer(this.pending[0].length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the pipe if there is a pending read operation with no more
|
||||
* buffered data to be read.
|
||||
*/
|
||||
maybeClose() {
|
||||
if (this.buffer) {
|
||||
let read = win32.DWORD();
|
||||
|
||||
let ok = libc.GetOverlappedResult(
|
||||
this.handle, this.overlapped.address(),
|
||||
read.address(), false);
|
||||
|
||||
if (!ok) {
|
||||
this.onError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously reads at most `length` bytes of binary data from the file
|
||||
* descriptor into an ArrayBuffer of the same size. Returns a promise which
|
||||
* resolves when the operation is complete.
|
||||
*
|
||||
* @param {integer} length
|
||||
* The number of bytes to read.
|
||||
*
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
*/
|
||||
read(length) {
|
||||
if (this.closing || this.closed) {
|
||||
throw new Error("Attempt to read from closed pipe");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.push({resolve, reject, length});
|
||||
this.readNext();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an overlapped IO read operation to read exactly `count` bytes
|
||||
* into a new ArrayBuffer, which is stored in the `buffer` property until the
|
||||
* operation completes.
|
||||
*
|
||||
* @param {integer} count
|
||||
* The number of bytes to read.
|
||||
*/
|
||||
readBuffer(count) {
|
||||
this.buffer = new ArrayBuffer(count);
|
||||
|
||||
let ok = libc.ReadFile(this.handle, this.buffer, count,
|
||||
null, this.overlapped.address());
|
||||
|
||||
if (!ok && (!this.process.handle || libc.winLastError)) {
|
||||
this.onError();
|
||||
} else {
|
||||
io.updatePollEvents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when our pending overlapped IO operation has completed, whether
|
||||
* successfully or in failure.
|
||||
*/
|
||||
onReady() {
|
||||
let read = win32.DWORD();
|
||||
|
||||
let ok = libc.GetOverlappedResult(
|
||||
this.handle, this.overlapped.address(),
|
||||
read.address(), false);
|
||||
|
||||
read = read.value;
|
||||
|
||||
if (!ok) {
|
||||
this.onError();
|
||||
} else if (read > 0) {
|
||||
let buffer = this.buffer;
|
||||
this.buffer = null;
|
||||
|
||||
let {resolve} = this.shiftPending();
|
||||
|
||||
if (read == buffer.byteLength) {
|
||||
resolve(buffer);
|
||||
} else {
|
||||
resolve(ArrayBuffer.transfer(buffer, read));
|
||||
}
|
||||
|
||||
if (this.pending.length) {
|
||||
this.readNext();
|
||||
} else {
|
||||
io.updatePollEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OutputPipe extends Pipe {
|
||||
/**
|
||||
* Queues the next chunk of data to be written to the pipe if, and only if,
|
||||
* there is no IO operation currently pending.
|
||||
*/
|
||||
writeNext() {
|
||||
if (this.buffer === null) {
|
||||
this.writeBuffer(this.pending[0].buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously writes the given buffer to our file descriptor, and returns
|
||||
* a promise which resolves when the operation is complete.
|
||||
*
|
||||
* @param {ArrayBuffer} buffer
|
||||
* The buffer to write.
|
||||
*
|
||||
* @returns {Promise<integer>}
|
||||
* Resolves to the number of bytes written when the operation is
|
||||
* complete.
|
||||
*/
|
||||
write(buffer) {
|
||||
if (this.closing || this.closed) {
|
||||
throw new Error("Attempt to write to closed pipe");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.push({resolve, reject, buffer});
|
||||
this.writeNext();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an overapped IO read operation to write the data in `buffer` to
|
||||
* our file descriptor.
|
||||
*
|
||||
* @param {ArrayBuffer} buffer
|
||||
* The buffer to write.
|
||||
*/
|
||||
writeBuffer(buffer) {
|
||||
this.buffer = buffer;
|
||||
|
||||
let ok = libc.WriteFile(this.handle, buffer, buffer.byteLength,
|
||||
null, this.overlapped.address());
|
||||
|
||||
if (!ok && libc.winLastError) {
|
||||
this.onError();
|
||||
} else {
|
||||
io.updatePollEvents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when our pending overlapped IO operation has completed, whether
|
||||
* successfully or in failure.
|
||||
*/
|
||||
onReady() {
|
||||
let written = win32.DWORD();
|
||||
|
||||
let ok = libc.GetOverlappedResult(
|
||||
this.handle, this.overlapped.address(),
|
||||
written.address(), false);
|
||||
|
||||
written = written.value;
|
||||
|
||||
if (!ok || written != this.buffer.byteLength) {
|
||||
this.onError();
|
||||
} else if (written > 0) {
|
||||
let {resolve} = this.shiftPending();
|
||||
|
||||
this.buffer = null;
|
||||
resolve(written);
|
||||
|
||||
if (this.pending.length) {
|
||||
this.writeNext();
|
||||
} else {
|
||||
io.updatePollEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Process extends BaseProcess {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.killed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns our process handle for use as an event in a WaitForMultipleObjects
|
||||
* call.
|
||||
*/
|
||||
get event() {
|
||||
return this.handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcibly terminates the process.
|
||||
*/
|
||||
kill() {
|
||||
this.killed = true;
|
||||
libc.TerminateProcess(this.handle, TERMINATE_EXIT_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the IO pipes for use as standard input, output, and error
|
||||
* descriptors in the spawned process.
|
||||
*
|
||||
* @returns {win32.Handle[]}
|
||||
* The array of file handles belonging to the spawned process.
|
||||
*/
|
||||
initPipes({stderr}) {
|
||||
let our_pipes = [];
|
||||
let their_pipes = [];
|
||||
|
||||
let secAttr = new win32.SECURITY_ATTRIBUTES();
|
||||
secAttr.nLength = win32.SECURITY_ATTRIBUTES.size;
|
||||
secAttr.bInheritHandle = true;
|
||||
|
||||
let pipe = input => {
|
||||
if (input) {
|
||||
let handles = win32.createPipe(secAttr, win32.FILE_FLAG_OVERLAPPED);
|
||||
our_pipes.push(new InputPipe(this, handles[0]));
|
||||
return handles[1];
|
||||
} else {
|
||||
let handles = win32.createPipe(secAttr, 0, win32.FILE_FLAG_OVERLAPPED);
|
||||
our_pipes.push(new OutputPipe(this, handles[1]));
|
||||
return handles[0];
|
||||
}
|
||||
};
|
||||
|
||||
their_pipes[0] = pipe(false);
|
||||
their_pipes[1] = pipe(true);
|
||||
|
||||
if (stderr == "pipe") {
|
||||
their_pipes[2] = pipe(true);
|
||||
} else {
|
||||
let srcHandle;
|
||||
if (stderr == "stdout") {
|
||||
srcHandle = their_pipes[1];
|
||||
} else {
|
||||
srcHandle = libc.GetStdHandle(win32.STD_ERROR_HANDLE);
|
||||
}
|
||||
|
||||
let handle = win32.HANDLE();
|
||||
|
||||
let curProc = libc.GetCurrentProcess();
|
||||
let ok = libc.DuplicateHandle(curProc, srcHandle, curProc, handle.address(),
|
||||
0, true /* inheritable */,
|
||||
win32.DUPLICATE_SAME_ACCESS);
|
||||
|
||||
their_pipes[2] = ok && win32.Handle(handle);
|
||||
}
|
||||
|
||||
if (!their_pipes.every(handle => handle)) {
|
||||
throw new Error("Failed to create pipe");
|
||||
}
|
||||
|
||||
this.pipes = our_pipes;
|
||||
|
||||
return their_pipes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a null-separated, null-terminated string list.
|
||||
*/
|
||||
stringList(strings) {
|
||||
// Remove empty strings, which would terminate the list early.
|
||||
strings = strings.filter(string => string);
|
||||
|
||||
let string = strings.join("\0") + "\0\0";
|
||||
|
||||
return win32.WCHAR.array()(string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes a string for use as a single command argument, using Windows quoting
|
||||
* conventions.
|
||||
*
|
||||
* @see https://msdn.microsoft.com/en-us/library/17w5ykft(v=vs.85).aspx
|
||||
*/
|
||||
quoteString(str) {
|
||||
if (!/[\s"]/.test(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
let escaped = str.replace(/(\\*)("|$)/g, (m0, m1, m2) => {
|
||||
if (m2) {
|
||||
m2 = `\\${m2}`;
|
||||
}
|
||||
return `${m1}${m1}${m2}`;
|
||||
});
|
||||
|
||||
return `"${escaped}"`;
|
||||
}
|
||||
|
||||
spawn(options) {
|
||||
let {command, arguments: args} = options;
|
||||
|
||||
args = args.map(arg => this.quoteString(arg));
|
||||
|
||||
let envp = this.stringList(options.environment);
|
||||
|
||||
let handles = this.initPipes(options);
|
||||
|
||||
let processFlags = win32.CREATE_NO_WINDOW
|
||||
| win32.CREATE_UNICODE_ENVIRONMENT;
|
||||
|
||||
let startupInfo = new win32.STARTUPINFOW();
|
||||
startupInfo.cb = win32.STARTUPINFOW.size;
|
||||
startupInfo.dwFlags = win32.STARTF_USESTDHANDLES;
|
||||
|
||||
startupInfo.hStdInput = handles[0];
|
||||
startupInfo.hStdOutput = handles[1];
|
||||
startupInfo.hStdError = handles[2];
|
||||
|
||||
let procInfo = new win32.PROCESS_INFORMATION();
|
||||
|
||||
let ok = libc.CreateProcessW(
|
||||
command, args.join(" "),
|
||||
null, /* Security attributes */
|
||||
null, /* Thread security attributes */
|
||||
true, /* Inherits handles */
|
||||
processFlags, envp, options.workdir,
|
||||
startupInfo.address(),
|
||||
procInfo.address());
|
||||
|
||||
for (let handle of new Set(handles)) {
|
||||
handle.dispose();
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
for (let pipe of this.pipes) {
|
||||
pipe.close();
|
||||
}
|
||||
throw new Error("Failed to create process");
|
||||
}
|
||||
|
||||
libc.CloseHandle(procInfo.hThread);
|
||||
|
||||
this.handle = win32.Handle(procInfo.hProcess);
|
||||
this.pid = procInfo.dwProcessId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when our process handle is signaled as active, meaning the process
|
||||
* has exited.
|
||||
*/
|
||||
onReady() {
|
||||
this.wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to wait for the process's exit status, without blocking. If
|
||||
* successful, resolves the `exitPromise` to the process's exit value.
|
||||
*
|
||||
* @returns {integer|null}
|
||||
* The process's exit status, if it has already exited.
|
||||
*/
|
||||
wait() {
|
||||
if (this.exitCode !== null) {
|
||||
return this.exitCode;
|
||||
}
|
||||
|
||||
let status = win32.DWORD();
|
||||
|
||||
let ok = libc.GetExitCodeProcess(this.handle, status.address());
|
||||
if (ok && status.value != win32.STILL_ACTIVE) {
|
||||
let exitCode = status.value;
|
||||
if (this.killed && exitCode == TERMINATE_EXIT_CODE) {
|
||||
// If we forcibly terminated the process, return the force kill exit
|
||||
// code that we return on other platforms.
|
||||
exitCode = -9;
|
||||
}
|
||||
|
||||
this.resolveExit(exitCode);
|
||||
this.exitCode = exitCode;
|
||||
|
||||
this.handle.dispose();
|
||||
this.handle = null;
|
||||
|
||||
for (let pipe of this.pipes) {
|
||||
pipe.maybeClose();
|
||||
}
|
||||
|
||||
io.updatePollEvents();
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
io = {
|
||||
events: null,
|
||||
eventHandlers: null,
|
||||
|
||||
pipes: new Map(),
|
||||
|
||||
processes: new Map(),
|
||||
|
||||
interval: null,
|
||||
|
||||
getPipe(pipeId) {
|
||||
let pipe = this.pipes.get(pipeId);
|
||||
|
||||
if (!pipe) {
|
||||
let error = new Error("File closed");
|
||||
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
|
||||
throw error;
|
||||
}
|
||||
return pipe;
|
||||
},
|
||||
|
||||
getProcess(processId) {
|
||||
let process = this.processes.get(processId);
|
||||
|
||||
if (!process) {
|
||||
throw new Error(`Invalid process ID: ${processId}`);
|
||||
}
|
||||
return process;
|
||||
},
|
||||
|
||||
updatePollEvents() {
|
||||
let handlers = [...this.pipes.values(),
|
||||
...this.processes.values()];
|
||||
|
||||
handlers = handlers.filter(handler => handler.event);
|
||||
|
||||
this.eventHandlers = handlers;
|
||||
|
||||
let handles = handlers.map(handler => handler.event);
|
||||
this.events = win32.HANDLE.array()(handles);
|
||||
|
||||
if (handles.length && !this.interval) {
|
||||
this.interval = setInterval(this.poll.bind(this), POLL_INTERVAL);
|
||||
} else if (!handlers.length && this.interval) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
},
|
||||
|
||||
poll() {
|
||||
for (;;) {
|
||||
let events = this.events;
|
||||
let handlers = this.eventHandlers;
|
||||
|
||||
let result = libc.WaitForMultipleObjects(events.length, events,
|
||||
false, POLL_TIMEOUT);
|
||||
|
||||
if (result < handlers.length) {
|
||||
try {
|
||||
handlers[result].onReady();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
debug(`Worker error: ${e} :: ${e.stack}`);
|
||||
handlers[result].onError();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addProcess(process) {
|
||||
this.processes.set(process.id, process);
|
||||
|
||||
for (let pipe of process.pipes) {
|
||||
this.pipes.set(pipe.id, pipe);
|
||||
}
|
||||
},
|
||||
|
||||
cleanupProcess(process) {
|
||||
this.processes.delete(process.id);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../../../../testing/xpcshell/xpcshell.eslintrc",
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env python2
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import signal
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def output(line):
|
||||
sys.stdout.write(struct.pack('@I', len(line)))
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def echo_loop():
|
||||
while True:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
output(line)
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
|
||||
|
||||
|
||||
cmd = sys.argv[1]
|
||||
if cmd == 'echo':
|
||||
echo_loop()
|
||||
elif cmd == 'exit':
|
||||
sys.exit(int(sys.argv[2]))
|
||||
elif cmd == 'env':
|
||||
for var in sys.argv[2:]:
|
||||
output(os.environ.get(var, ''))
|
||||
elif cmd == 'pwd':
|
||||
output(os.path.abspath(os.curdir))
|
||||
elif cmd == 'print_args':
|
||||
for arg in sys.argv[2:]:
|
||||
output(arg)
|
||||
elif cmd == 'ignore_sigterm':
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
|
||||
output('Ready')
|
||||
while True:
|
||||
try:
|
||||
signal.pause()
|
||||
except AttributeError:
|
||||
import time
|
||||
time.sleep(3600)
|
||||
elif cmd == 'print':
|
||||
sys.stdout.write(sys.argv[2])
|
||||
sys.stderr.write(sys.argv[3])
|
|
@ -0,0 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Subprocess",
|
||||
"resource://gre/modules/Subprocess.jsm");
|
|
@ -0,0 +1,682 @@
|
|||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
|
||||
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||
|
||||
let PYTHON;
|
||||
let PYTHON_BIN;
|
||||
let PYTHON_DIR;
|
||||
|
||||
const TEST_SCRIPT = do_get_file("data_test_script.py").path;
|
||||
|
||||
let read = pipe => {
|
||||
return pipe.readUint32().then(count => {
|
||||
return pipe.readString(count);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
let readAll = Task.async(function* (pipe) {
|
||||
let result = [];
|
||||
let string;
|
||||
while ((string = yield pipe.readString())) {
|
||||
result.push(string);
|
||||
}
|
||||
|
||||
return result.join("");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* setup() {
|
||||
PYTHON = yield Subprocess.pathSearch(env.get("PYTHON"));
|
||||
|
||||
PYTHON_BIN = OS.Path.basename(PYTHON);
|
||||
PYTHON_DIR = OS.Path.dirname(PYTHON);
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_io() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
Assert.throws(() => { proc.stdout.read(-1); },
|
||||
/non-negative integer/);
|
||||
Assert.throws(() => { proc.stdout.read(1.1); },
|
||||
/non-negative integer/);
|
||||
|
||||
Assert.throws(() => { proc.stdout.read(Infinity); },
|
||||
/non-negative integer/);
|
||||
Assert.throws(() => { proc.stdout.read(NaN); },
|
||||
/non-negative integer/);
|
||||
|
||||
Assert.throws(() => { proc.stdout.readString(-1); },
|
||||
/non-negative integer/);
|
||||
Assert.throws(() => { proc.stdout.readString(1.1); },
|
||||
/non-negative integer/);
|
||||
|
||||
Assert.throws(() => { proc.stdout.readJSON(-1); },
|
||||
/positive integer/);
|
||||
Assert.throws(() => { proc.stdout.readJSON(0); },
|
||||
/positive integer/);
|
||||
Assert.throws(() => { proc.stdout.readJSON(1.1); },
|
||||
/positive integer/);
|
||||
|
||||
|
||||
const LINE1 = "I'm a leaf on the wind.\n";
|
||||
const LINE2 = "Watch how I soar.\n";
|
||||
|
||||
|
||||
let outputPromise = read(proc.stdout);
|
||||
|
||||
yield new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
let [output] = yield Promise.all([
|
||||
outputPromise,
|
||||
proc.stdin.write(LINE1),
|
||||
]);
|
||||
|
||||
equal(output, LINE1, "Got expected output");
|
||||
|
||||
|
||||
// Make sure it succeeds whether the write comes before or after the
|
||||
// read.
|
||||
let inputPromise = proc.stdin.write(LINE2);
|
||||
|
||||
yield new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
[output] = yield Promise.all([
|
||||
read(proc.stdout),
|
||||
inputPromise,
|
||||
]);
|
||||
|
||||
equal(output, LINE2, "Got expected output");
|
||||
|
||||
|
||||
let JSON_BLOB = {foo: {bar: "baz"}};
|
||||
|
||||
inputPromise = proc.stdin.write(JSON.stringify(JSON_BLOB) + "\n");
|
||||
|
||||
output = yield proc.stdout.readUint32().then(count => {
|
||||
return proc.stdout.readJSON(count);
|
||||
});
|
||||
|
||||
Assert.deepEqual(output, JSON_BLOB, "Got expected JSON output");
|
||||
|
||||
|
||||
yield proc.stdin.close();
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_large_io() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
const LINE = "I'm a leaf on the wind.\n";
|
||||
const BUFFER_SIZE = 4096;
|
||||
|
||||
// Create a message that's ~3/4 the input buffer size.
|
||||
let msg = Array(BUFFER_SIZE * .75 / 16 | 0).fill("0123456789abcdef").join("") + "\n";
|
||||
|
||||
// This sequence of writes and reads crosses several buffer size
|
||||
// boundaries, and causes some branches of the read buffer code to be
|
||||
// exercised which are not exercised by other tests.
|
||||
proc.stdin.write(msg);
|
||||
proc.stdin.write(msg);
|
||||
proc.stdin.write(LINE);
|
||||
|
||||
let output = yield read(proc.stdout);
|
||||
equal(output, msg, "Got the expected output");
|
||||
|
||||
output = yield read(proc.stdout);
|
||||
equal(output, msg, "Got the expected output");
|
||||
|
||||
output = yield read(proc.stdout);
|
||||
equal(output, LINE, "Got the expected output");
|
||||
|
||||
proc.stdin.close();
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_huge() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
// This should be large enough to fill most pipe input/output buffers.
|
||||
const MESSAGE_SIZE = 1024 * 16;
|
||||
|
||||
let msg = Array(MESSAGE_SIZE).fill("0123456789abcdef").join("") + "\n";
|
||||
|
||||
proc.stdin.write(msg);
|
||||
|
||||
let output = yield read(proc.stdout);
|
||||
equal(output, msg, "Got the expected output");
|
||||
|
||||
proc.stdin.close();
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_stderr_default() {
|
||||
const LINE1 = "I'm a leaf on the wind.\n";
|
||||
const LINE2 = "Watch how I soar.\n";
|
||||
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
|
||||
});
|
||||
|
||||
equal(proc.stderr, undefined, "There should be no stderr pipe by default");
|
||||
|
||||
let stdout = yield readAll(proc.stdout);
|
||||
|
||||
equal(stdout, LINE1, "Got the expected stdout output");
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_stderr_pipe() {
|
||||
const LINE1 = "I'm a leaf on the wind.\n";
|
||||
const LINE2 = "Watch how I soar.\n";
|
||||
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
let [stdout, stderr] = yield Promise.all([
|
||||
readAll(proc.stdout),
|
||||
readAll(proc.stderr),
|
||||
]);
|
||||
|
||||
equal(stdout, LINE1, "Got the expected stdout output");
|
||||
equal(stderr, LINE2, "Got the expected stderr output");
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_stderr_merged() {
|
||||
const LINE1 = "I'm a leaf on the wind.\n";
|
||||
const LINE2 = "Watch how I soar.\n";
|
||||
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
|
||||
stderr: "stdout",
|
||||
});
|
||||
|
||||
equal(proc.stderr, undefined, "There should be no stderr pipe by default");
|
||||
|
||||
let stdout = yield readAll(proc.stdout);
|
||||
|
||||
equal(stdout, LINE1 + LINE2, "Got the expected merged stdout output");
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_read_after_exit() {
|
||||
const LINE1 = "I'm a leaf on the wind.\n";
|
||||
const LINE2 = "Watch how I soar.\n";
|
||||
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
equal(exitCode, 0, "Process exited with expected code");
|
||||
|
||||
|
||||
let [stdout, stderr] = yield Promise.all([
|
||||
readAll(proc.stdout),
|
||||
readAll(proc.stderr),
|
||||
]);
|
||||
|
||||
equal(stdout, LINE1, "Got the expected stdout output");
|
||||
equal(stderr, LINE2, "Got the expected stderr output");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_lazy_close_output() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
const LINE1 = "I'm a leaf on the wind.\n";
|
||||
const LINE2 = "Watch how I soar.\n";
|
||||
|
||||
let writePromises = [
|
||||
proc.stdin.write(LINE1),
|
||||
proc.stdin.write(LINE2),
|
||||
];
|
||||
let closedPromise = proc.stdin.close();
|
||||
|
||||
|
||||
let output1 = yield read(proc.stdout);
|
||||
let output2 = yield read(proc.stdout);
|
||||
|
||||
yield Promise.all([...writePromises, closedPromise]);
|
||||
|
||||
equal(output1, LINE1, "Got expected output");
|
||||
equal(output2, LINE2, "Got expected output");
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_lazy_close_input() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
let readPromise = proc.stdout.readUint32();
|
||||
let closedPromise = proc.stdout.close();
|
||||
|
||||
|
||||
const LINE = "I'm a leaf on the wind.\n";
|
||||
|
||||
proc.stdin.write(LINE);
|
||||
proc.stdin.close();
|
||||
|
||||
let len = yield readPromise;
|
||||
equal(len, LINE.length);
|
||||
|
||||
yield closedPromise;
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_force_close() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
let readPromise = proc.stdout.readUint32();
|
||||
let closedPromise = proc.stdout.close(true);
|
||||
|
||||
yield Assert.rejects(
|
||||
readPromise,
|
||||
function(e) {
|
||||
equal(e.errorCode, Subprocess.ERROR_END_OF_FILE,
|
||||
"Got the expected error code");
|
||||
return /File closed/.test(e.message);
|
||||
},
|
||||
"Promise should be rejected when file is closed");
|
||||
|
||||
yield closedPromise;
|
||||
yield proc.stdin.close();
|
||||
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_eof() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
let readPromise = proc.stdout.readUint32();
|
||||
|
||||
yield proc.stdin.close();
|
||||
|
||||
yield Assert.rejects(
|
||||
readPromise,
|
||||
function(e) {
|
||||
equal(e.errorCode, Subprocess.ERROR_END_OF_FILE,
|
||||
"Got the expected error code");
|
||||
return /File closed/.test(e.message);
|
||||
},
|
||||
"Promise should be rejected on EOF");
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_invalid_json() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
const LINE = "I'm a leaf on the wind.\n";
|
||||
|
||||
proc.stdin.write(LINE);
|
||||
proc.stdin.close();
|
||||
|
||||
let count = yield proc.stdout.readUint32();
|
||||
let readPromise = proc.stdout.readJSON(count);
|
||||
|
||||
yield Assert.rejects(
|
||||
readPromise,
|
||||
function(e) {
|
||||
equal(e.errorCode, Subprocess.ERROR_INVALID_JSON,
|
||||
"Got the expected error code");
|
||||
return /SyntaxError/.test(e);
|
||||
},
|
||||
"Promise should be rejected on EOF");
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_wait() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "exit", "42"],
|
||||
});
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 42, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_pathSearch() {
|
||||
let promise = Subprocess.call({
|
||||
command: PYTHON_BIN,
|
||||
arguments: ["-u", TEST_SCRIPT, "exit", "13"],
|
||||
environment: {
|
||||
PATH: PYTHON_DIR,
|
||||
},
|
||||
});
|
||||
|
||||
yield Assert.rejects(
|
||||
promise,
|
||||
function(error) {
|
||||
return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
|
||||
},
|
||||
"Subprocess.call should fail for a bad executable");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_workdir() {
|
||||
let procDir = yield OS.File.getCurrentDirectory();
|
||||
let tmpDir = OS.Constants.Path.tmpDir;
|
||||
|
||||
notEqual(procDir, tmpDir,
|
||||
"Current process directory must not be the current temp directory");
|
||||
|
||||
function* pwd(options) {
|
||||
let proc = yield Subprocess.call(Object.assign({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "pwd"],
|
||||
}, options));
|
||||
|
||||
let pwd = read(proc.stdout);
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
|
||||
return pwd;
|
||||
}
|
||||
|
||||
let dir = yield pwd({});
|
||||
equal(dir, procDir, "Process should normally launch in current process directory");
|
||||
|
||||
dir = yield pwd({workdir: tmpDir});
|
||||
equal(dir, tmpDir, "Process should launch in the directory specified in `workdir`");
|
||||
|
||||
dir = yield OS.File.getCurrentDirectory();
|
||||
equal(dir, procDir, "`workdir` should not change the working directory of the current process");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_term() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
// Windows does not support killing processes gracefully, so they will
|
||||
// always exit with -9 there.
|
||||
let retVal = AppConstants.platform == "win" ? -9 : -15;
|
||||
|
||||
// Kill gracefully with the default timeout of 300ms.
|
||||
let {exitCode} = yield proc.kill();
|
||||
|
||||
equal(exitCode, retVal, "Got expected exit code");
|
||||
|
||||
({exitCode} = yield proc.wait());
|
||||
|
||||
equal(exitCode, retVal, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_kill() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "echo"],
|
||||
});
|
||||
|
||||
// Force kill with no gracefull termination timeout.
|
||||
let {exitCode} = yield proc.kill(0);
|
||||
|
||||
equal(exitCode, -9, "Got expected exit code");
|
||||
|
||||
({exitCode} = yield proc.wait());
|
||||
|
||||
equal(exitCode, -9, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_kill_timeout() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "ignore_sigterm"],
|
||||
});
|
||||
|
||||
// Wait for the process to set up its signal handler and tell us it's
|
||||
// ready.
|
||||
let msg = yield read(proc.stdout);
|
||||
equal(msg, "Ready", "Process is ready");
|
||||
|
||||
// Kill gracefully with the default timeout of 300ms.
|
||||
// Expect a force kill after 300ms, since the process traps SIGTERM.
|
||||
const TIMEOUT = 300;
|
||||
let startTime = Date.now();
|
||||
|
||||
let {exitCode} = yield proc.kill(TIMEOUT);
|
||||
|
||||
// Graceful termination is not supported on Windows, so don't bother
|
||||
// testing the timeout there.
|
||||
if (AppConstants.platform != "win") {
|
||||
let diff = Date.now() - startTime;
|
||||
ok(diff >= TIMEOUT, `Process was killed after ${diff}ms (expected ~${TIMEOUT}ms)`);
|
||||
}
|
||||
|
||||
equal(exitCode, -9, "Got expected exit code");
|
||||
|
||||
({exitCode} = yield proc.wait());
|
||||
|
||||
equal(exitCode, -9, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_subprocess_arguments() {
|
||||
let args = [
|
||||
String.raw`C:\Program Files\Company\Program.exe`,
|
||||
String.raw`\\NETWORK SHARE\Foo Directory${"\\"}`,
|
||||
String.raw`foo bar baz`,
|
||||
String.raw`"foo bar baz"`,
|
||||
String.raw`foo " bar`,
|
||||
String.raw`Thing \" with "" "\" \\\" \\\\" quotes\\" \\`,
|
||||
];
|
||||
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "print_args", ...args],
|
||||
});
|
||||
|
||||
for (let [i, arg] of args.entries()) {
|
||||
let val = yield read(proc.stdout);
|
||||
equal(val, arg, `Got correct value for args[${i}]`);
|
||||
}
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
// Windows XP can't handle launching Python with a partial environment.
|
||||
if (!AppConstants.isPlatformAndVersionAtMost("win", "5.2")) {
|
||||
add_task(function* test_subprocess_environment() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
|
||||
environment: {
|
||||
FOO: "BAR",
|
||||
},
|
||||
});
|
||||
|
||||
let path = yield read(proc.stdout);
|
||||
let foo = yield read(proc.stdout);
|
||||
|
||||
equal(path, "", "Got expected $PATH value");
|
||||
equal(foo, "BAR", "Got expected $FOO value");
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
add_task(function* test_subprocess_environmentAppend() {
|
||||
let proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
|
||||
environmentAppend: true,
|
||||
environment: {
|
||||
FOO: "BAR",
|
||||
},
|
||||
});
|
||||
|
||||
let path = yield read(proc.stdout);
|
||||
let foo = yield read(proc.stdout);
|
||||
|
||||
equal(path, env.get("PATH"), "Got expected $PATH value");
|
||||
equal(foo, "BAR", "Got expected $FOO value");
|
||||
|
||||
let {exitCode} = yield proc.wait();
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
|
||||
proc = yield Subprocess.call({
|
||||
command: PYTHON,
|
||||
arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
|
||||
environmentAppend: true,
|
||||
});
|
||||
|
||||
path = yield read(proc.stdout);
|
||||
foo = yield read(proc.stdout);
|
||||
|
||||
equal(path, env.get("PATH"), "Got expected $PATH value");
|
||||
equal(foo, "", "Got expected $FOO value");
|
||||
|
||||
({exitCode} = yield proc.wait());
|
||||
|
||||
equal(exitCode, 0, "Got expected exit code");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_bad_executable() {
|
||||
// Test with a non-executable file.
|
||||
|
||||
let textFile = do_get_file("data_text_file.txt").path;
|
||||
|
||||
let promise = Subprocess.call({
|
||||
command: textFile,
|
||||
arguments: [],
|
||||
});
|
||||
|
||||
yield Assert.rejects(
|
||||
promise,
|
||||
function(error) {
|
||||
if (AppConstants.platform == "win") {
|
||||
return /Failed to create process/.test(error.message);
|
||||
}
|
||||
return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
|
||||
},
|
||||
"Subprocess.call should fail for a bad executable");
|
||||
|
||||
// Test with a nonexistent file.
|
||||
promise = Subprocess.call({
|
||||
command: textFile + ".doesNotExist",
|
||||
arguments: [],
|
||||
});
|
||||
|
||||
yield Assert.rejects(
|
||||
promise,
|
||||
function(error) {
|
||||
return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
|
||||
},
|
||||
"Subprocess.call should fail for a bad executable");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_cleanup() {
|
||||
let {SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
|
||||
|
||||
let worker = SubprocessImpl.Process.getWorker();
|
||||
|
||||
let openFiles = yield worker.call("getOpenFiles", []);
|
||||
let processes = yield worker.call("getProcesses", []);
|
||||
|
||||
equal(openFiles.size, 0, "No remaining open files");
|
||||
equal(processes.size, 0, "No remaining processes");
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
"use strict";
|
||||
|
||||
let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||
|
||||
add_task(function* test_getEnvironment() {
|
||||
env.set("FOO", "BAR");
|
||||
|
||||
let environment = Subprocess.getEnvironment();
|
||||
|
||||
equal(environment.FOO, "BAR");
|
||||
equal(environment.PATH, env.get("PATH"));
|
||||
|
||||
env.set("FOO", null);
|
||||
|
||||
environment = Subprocess.getEnvironment();
|
||||
equal(environment.FOO || "", "");
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
"use strict";
|
||||
|
||||
let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||
|
||||
const PYTHON = env.get("PYTHON");
|
||||
|
||||
const PYTHON_BIN = OS.Path.basename(PYTHON);
|
||||
const PYTHON_DIR = OS.Path.dirname(PYTHON);
|
||||
|
||||
const DOES_NOT_EXIST = OS.Path.join(OS.Constants.Path.tmpDir,
|
||||
"ThisPathDoesNotExist");
|
||||
|
||||
const PATH_SEP = AppConstants.platform == "win" ? ";" : ":";
|
||||
|
||||
|
||||
add_task(function* test_pathSearchAbsolute() {
|
||||
let env = {};
|
||||
|
||||
let path = yield Subprocess.pathSearch(PYTHON, env);
|
||||
equal(path, PYTHON, "Full path resolves even with no PATH.");
|
||||
|
||||
env.PATH = "";
|
||||
path = yield Subprocess.pathSearch(PYTHON, env);
|
||||
equal(path, PYTHON, "Full path resolves even with empty PATH.");
|
||||
|
||||
yield Assert.rejects(
|
||||
Subprocess.pathSearch(DOES_NOT_EXIST, env),
|
||||
function(e) {
|
||||
equal(e.errorCode, Subprocess.ERROR_BAD_EXECUTABLE,
|
||||
"Got the expected error code");
|
||||
return /File at path .* does not exist, or is not (executable|a normal file)/.test(e.message);
|
||||
},
|
||||
"Absolute path should throw for a nonexistent execuable");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_pathSearchRelative() {
|
||||
let env = {};
|
||||
|
||||
yield Assert.rejects(
|
||||
Subprocess.pathSearch(PYTHON_BIN, env),
|
||||
function(e) {
|
||||
equal(e.errorCode, Subprocess.ERROR_BAD_EXECUTABLE,
|
||||
"Got the expected error code");
|
||||
return /Executable not found:/.test(e.message);
|
||||
},
|
||||
"Relative path should not be found when PATH is missing");
|
||||
|
||||
env.PATH = [DOES_NOT_EXIST, PYTHON_DIR].join(PATH_SEP);
|
||||
|
||||
let path = yield Subprocess.pathSearch(PYTHON_BIN, env);
|
||||
equal(path, PYTHON, "Correct executable should be found in the path");
|
||||
});
|
||||
|
||||
|
||||
add_task({
|
||||
skip_if: () => AppConstants.platform != "win",
|
||||
}, function* test_pathSearch_PATHEXT() {
|
||||
ok(PYTHON_BIN.endsWith(".exe"), "Python executable must end with .exe");
|
||||
|
||||
const python_bin = PYTHON_BIN.slice(0, -4);
|
||||
|
||||
let env = {
|
||||
PATH: PYTHON_DIR,
|
||||
PATHEXT: [".com", ".exe", ".foobar"].join(";"),
|
||||
};
|
||||
|
||||
let path = yield Subprocess.pathSearch(python_bin, env);
|
||||
equal(path, PYTHON, "Correct executable should be found in the path, with guessed extension");
|
||||
});
|
||||
// IMPORTANT: Do not add any tests beyond this point without removing
|
||||
// the `skip_if` condition from the previous task, or it will prevent
|
||||
// all succeeding tasks from running when it does not match.
|
|
@ -0,0 +1,13 @@
|
|||
[DEFAULT]
|
||||
head = head.js
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
skip-if = os == 'android'
|
||||
subprocess = true
|
||||
support-files =
|
||||
data_text_file.txt
|
||||
data_test_script.py
|
||||
|
||||
[test_subprocess.js]
|
||||
[test_subprocess_getEnvironment.js]
|
||||
[test_subprocess_pathSearch.js]
|
|
@ -5097,10 +5097,20 @@
|
|||
Push $R6
|
||||
Push $R5
|
||||
|
||||
; 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.
|
||||
System::Call "kernel32::IsProcessorFeaturePresent(i 10)i .R8"
|
||||
${If} "$R8" == "0"
|
||||
MessageBox MB_OK|MB_ICONSTOP "$R9"
|
||||
; Nothing initialized so no need to call OnEndCommon
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
!ifdef HAVE_64BIT_BUILD
|
||||
${Unless} ${RunningX64}
|
||||
${OrUnless} ${AtLeastWin7}
|
||||
MessageBox MB_OK|MB_ICONSTOP "$R9" IDOK
|
||||
MessageBox MB_OK|MB_ICONSTOP "$R9"
|
||||
; Nothing initialized so no need to call OnEndCommon
|
||||
Quit
|
||||
${EndUnless}
|
||||
|
@ -5132,7 +5142,7 @@
|
|||
${OrIf} "$R8" == "3"
|
||||
${OrIf} "$R8" == "4"
|
||||
${OrIf} "$R8" == "5"
|
||||
MessageBox MB_OK|MB_ICONSTOP "$R9" IDOK
|
||||
MessageBox MB_OK|MB_ICONSTOP "$R9"
|
||||
; Nothing initialized so no need to call OnEndCommon
|
||||
Quit
|
||||
${EndIf}
|
||||
|
|
Загрузка…
Ссылка в новой задаче