merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2016-05-30 11:54:53 +02:00
Родитель 50301d068f d03a76f2e5
Коммит b4d7358820
74 изменённых файлов: 5857 добавлений и 515 удалений

Просмотреть файл

@ -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}