Make sure that SettingFile arg is parsed before we load the settings (#7449)

Make sure that SettingFile arg is parsed before we load the settings

- Add ability to parse some setting before console host is created
- Trigger the Parse before the console host is created
- disable snapin loading from loading setting before this point
- re-enable mac logging tests.

Also, fix the following Travis-ci issues:

- make sure the Linux and macOS caches are separate to reduce the size
- update the macOS image to the closest image as used by the official build system (equivalent to VSTS hosted mac builds)
This commit is contained in:
Travis Plunk 2018-08-07 15:01:31 -07:00 committed by GitHub
parent 0c11582e6c
commit 1523c218a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 306 additions and 139 deletions

View file

@ -8,8 +8,10 @@ matrix:
- os: linux
dist: trusty
sudo: required
env: TRAVIS_CI_CACHE_NAME=linux
- os: osx
osx_image: xcode8.1
osx_image: xcode9.4
env: TRAVIS_CI_CACHE_NAME=macOS
fast_finish: true
addons:

View file

@ -200,6 +200,7 @@ namespace Microsoft.PowerShell
_helpText = helpText;
}
#region Internal properties
internal bool AbortStartup
{
get
@ -393,6 +394,205 @@ namespace Microsoft.PowerShell
get { return _workingDirectory; }
}
#endregion Internal properties
#region static methods
/// <summary>
/// Processes the -SettingFile Argument.
/// </summary>
/// <param name="args">
/// The command line parameters to be processed.
/// </param>
/// <param name="settingFileArgIndex">
/// The index in args to the argument following '-SettingFile'.
/// </param>
/// <param name="parser">
/// Used to allow the helper to write errors to the console. If not supplied, no errors will be written.
/// </param>
/// <returns>
/// Returns true if the argument was parsed successfully and false if not.
/// </returns>
private static bool TryParseSettingFileHelper(string[] args, int settingFileArgIndex, CommandLineParameterParser parser)
{
if (settingFileArgIndex >= args.Length)
{
if (parser != null)
{
parser.WriteCommandLineError(
CommandLineParameterParserStrings.MissingSettingsFileArgument);
}
return false;
}
string configFile = null;
try
{
configFile = NormalizeFilePath(args[settingFileArgIndex]);
}
catch (Exception ex)
{
if (parser != null)
{
string error = string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.InvalidSettingsFileArgument, args[settingFileArgIndex], ex.Message);
parser.WriteCommandLineError(error);
}
return false;
}
if (!System.IO.File.Exists(configFile))
{
if (parser != null)
{
string error = string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.SettingsFileNotExists, configFile);
parser.WriteCommandLineError(error);
}
return false;
}
PowerShellConfig.Instance.SetSystemConfigFilePath(configFile);
return true;
}
/// <summary>
/// Processes the command line parameters to ConsoleHost which must be parsed before the Host is created.
/// Success to indicate that the program should continue running.
/// </summary>
/// <param name="args">
/// The command line parameters to be processed.
/// </param>
internal static void EarlyParse(string[] args)
{
// indicates that we've called this method on this instance, and that when it's done, the state variables
// will reflect the parse.
EarlyParseHelper(args);
}
private static string GetConfigurationNameFromGroupPolicy()
{
// Current user policy takes precedence.
var consoleSessionSetting = Utils.GetPolicySetting<ConsoleSessionConfiguration>(Utils.CurrentUserThenSystemWideConfig);
return (consoleSessionSetting?.EnableConsoleSessionConfiguration == true && !string.IsNullOrEmpty(consoleSessionSetting?.ConsoleSessionConfigurationName)) ?
consoleSessionSetting.ConsoleSessionConfigurationName : string.Empty;
}
/// <summary>
/// Processes the command line parameters to ConsoleHost which must be parsed before the Host is created.
/// Success to indicate that the program should continue running.
/// </summary>
/// <param name="args">
/// The command line parameters to be processed.
/// </param>
private static void EarlyParseHelper(string[] args)
{
if(args == null)
{
Dbg.Assert(args != null, "Argument 'args' to EarlyParseHelper should never be null");
return;
}
bool noexitSeen = false;
for (int i = 0; i < args.Length; ++i)
{
(string SwitchKey, bool ShouldBreak) switchKeyResults = GetSwitchKey(args, ref i, parser: null, ref noexitSeen);
if (switchKeyResults.ShouldBreak)
{
break;
}
string switchKey = switchKeyResults.SwitchKey;
if (MatchSwitch(switchKey, match: "settingsfile", smallestUnambiguousMatch: "settings"))
{
// parse setting file arg and don't write error as there is no host yet.
if (!TryParseSettingFileHelper(args, ++i, parser: null))
{
break;
}
}
}
}
/// <summary>
/// Gets the word in a switch from the current argument or parses a file.
/// For example -foo, /foo, or --foo would return 'foo'.
/// </summary>
/// <param name="args">
/// The command line parameters to be processed.
/// </param>
/// <param name="argIndex">
/// The index in args to the argument to process.
/// </param>
/// <param name="parser">
/// Used to parse files in the args. If not supplied, Files will not be parsed.
/// </param>
/// <param name="noexitSeen">
/// Used during parsing files.
/// </param>
/// <returns>
/// Returns a Tuple:
/// The first value is a String called SwitchKey with the word in a switch from the current argument or null.
/// The second value is a bool called ShouldBreak, indicating if the parsing look should break.
/// </returns>
private static (string SwitchKey, bool ShouldBreak) GetSwitchKey(string[] args, ref int argIndex, CommandLineParameterParser parser, ref bool noexitSeen)
{
string switchKey = args[argIndex].Trim().ToLowerInvariant();
if (string.IsNullOrEmpty(switchKey))
{
return (SwitchKey: null, ShouldBreak: false);
}
if (!SpecialCharacters.IsDash(switchKey[0]) && switchKey[0] != '/')
{
// then its a file
if (parser != null)
{
--argIndex;
parser.ParseFile(args, ref argIndex, noexitSeen);
}
return (SwitchKey: null, ShouldBreak: true);
}
// chop off the first character so that we're agnostic wrt specifying / or -
// in front of the switch name.
switchKey = switchKey.Substring(1);
// chop off the second dash so we're agnostic wrt specifying - or --
if (!string.IsNullOrEmpty(switchKey) && SpecialCharacters.IsDash(switchKey[0]))
{
switchKey = switchKey.Substring(1);
}
return (SwitchKey: switchKey, ShouldBreak: false);
}
private static string NormalizeFilePath(string path)
{
// Normalize slashes
path = path.Replace(StringLiterals.AlternatePathSeparator,
StringLiterals.DefaultPathSeparator);
return Path.GetFullPath(path);
}
private static bool MatchSwitch(string switchKey, string match, string smallestUnambiguousMatch)
{
Dbg.Assert(switchKey != null, "need a value");
Dbg.Assert(!String.IsNullOrEmpty(match), "need a value");
Dbg.Assert(match.Trim().ToLowerInvariant() == match, "match should be normalized to lowercase w/ no outside whitespace");
Dbg.Assert(smallestUnambiguousMatch.Trim().ToLowerInvariant() == smallestUnambiguousMatch, "match should be normalized to lowercase w/ no outside whitespace");
Dbg.Assert(match.Contains(smallestUnambiguousMatch), "sUM should be a substring of match");
return (match.Trim().ToLowerInvariant().IndexOf(switchKey, StringComparison.Ordinal) == 0 &&
switchKey.Length >= smallestUnambiguousMatch.Length);
}
#endregion
private void ShowHelp()
{
Dbg.Assert(_helpText != null, "_helpText should not be null");
@ -463,24 +663,6 @@ namespace Microsoft.PowerShell
}
}
private static string GetConfigurationNameFromGroupPolicy()
{
// Current user policy takes precedence.
var consoleSessionSetting = Utils.GetPolicySetting<ConsoleSessionConfiguration>(Utils.CurrentUserThenSystemWideConfig);
if (consoleSessionSetting != null)
{
if (consoleSessionSetting.EnableConsoleSessionConfiguration == true)
{
if (!string.IsNullOrEmpty(consoleSessionSetting.ConsoleSessionConfigurationName))
{
return consoleSessionSetting.ConsoleSessionConfigurationName;
}
}
}
return string.Empty;
}
private void ParseHelper(string[] args)
{
Dbg.Assert(args != null, "Argument 'args' to ParseHelper should never be null");
@ -488,32 +670,13 @@ namespace Microsoft.PowerShell
for (int i = 0; i < args.Length; ++i)
{
// Invariant culture used because command-line parameters are not localized.
string switchKey = args[i].Trim().ToLowerInvariant();
if (String.IsNullOrEmpty(switchKey))
(string SwitchKey, bool ShouldBreak) switchKeyResults = GetSwitchKey(args, ref i, this, ref noexitSeen);
if (switchKeyResults.ShouldBreak)
{
continue;
}
if (!SpecialCharacters.IsDash(switchKey[0]) && switchKey[0] != '/')
{
// then its a file
--i;
ParseFile(args, ref i, noexitSeen);
break;
}
// chop off the first character so that we're agnostic wrt specifying / or -
// in front of the switch name.
switchKey = switchKey.Substring(1);
// chop off the second dash so we're agnostic wrt specifying - or --
if (!String.IsNullOrEmpty(switchKey) && SpecialCharacters.IsDash(switchKey[0]))
{
switchKey = switchKey.Substring(1);
}
string switchKey = switchKeyResults.SwitchKey;
// If version is in the commandline, don't continue to look at any other parameters
if (MatchSwitch(switchKey, "version", "v"))
@ -711,34 +874,13 @@ namespace Microsoft.PowerShell
}
}
else if (MatchSwitch(switchKey, "settingsfile", "settings") )
else if (MatchSwitch(switchKey, "settingsfile", "settings"))
{
++i;
if (i >= args.Length)
// Parse setting file arg and write error
if (!TryParseSettingFileHelper(args, ++i, this))
{
WriteCommandLineError(
CommandLineParameterParserStrings.MissingSettingsFileArgument);
break;
}
string configFile = null;
try
{
configFile = NormalizeFilePath(args[i]);
}
catch (Exception ex)
{
string error = string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.InvalidSettingsFileArgument, args[i], ex.Message);
WriteCommandLineError(error);
break;
}
if (!System.IO.File.Exists(configFile))
{
string error = string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.SettingsFileNotExists, configFile);
WriteCommandLineError(error);
break;
}
PowerShellConfig.Instance.SetSystemConfigFilePath(configFile);
}
#if STAMODE
// explicit setting of the ApartmentState Not supported on NanoServer
@ -818,25 +960,6 @@ namespace Microsoft.PowerShell
_exitCode = ConsoleHost.ExitCodeBadCommandLineParameter;
}
private bool MatchSwitch(string switchKey, string match, string smallestUnambiguousMatch)
{
Dbg.Assert(switchKey != null, "need a value");
Dbg.Assert(!String.IsNullOrEmpty(match), "need a value");
Dbg.Assert(match.Trim().ToLowerInvariant() == match, "match should be normalized to lowercase w/ no outside whitespace");
Dbg.Assert(smallestUnambiguousMatch.Trim().ToLowerInvariant() == smallestUnambiguousMatch, "match should be normalized to lowercase w/ no outside whitespace");
Dbg.Assert(match.Contains(smallestUnambiguousMatch), "sUM should be a substring of match");
if (match.Trim().ToLowerInvariant().IndexOf(switchKey, StringComparison.Ordinal) == 0)
{
if (switchKey.Length >= smallestUnambiguousMatch.Length)
{
return true;
}
}
return false;
}
private void ParseFormat(string[] args, ref int i, ref Serialization.DataFormat format, string resourceStr)
{
StringBuilder sb = new StringBuilder();
@ -892,15 +1015,6 @@ namespace Microsoft.PowerShell
executionPolicy = args[i];
}
private static string NormalizeFilePath(string path)
{
// Normalize slashes
path = path.Replace(StringLiterals.AlternatePathSeparator,
StringLiterals.DefaultPathSeparator);
return Path.GetFullPath(path);
}
private bool ParseFile(string[] args, ref int i, bool noexitSeen)
{
// Process file execution. We don't need to worry about checking -command

View file

@ -148,6 +148,12 @@ namespace Microsoft.PowerShell
try
{
// We need to read the settings file before we create the console host
string[] tempArgs = new string[args.GetLength(0)];
args.CopyTo(tempArgs, 0);
CommandLineParameterParser.EarlyParse(tempArgs);
// We might be able to ignore console host creation error if we are running in
// server mode, which does not require a console.
HostException hostException = null;
@ -168,8 +174,6 @@ namespace Microsoft.PowerShell
s_cpp = new CommandLineParameterParser(
(s_theConsoleHost != null) ? s_theConsoleHost.UI : (new NullHostUserInterface()),
bannerText, helpText);
string[] tempArgs = new string[args.GetLength(0)];
args.CopyTo(tempArgs, 0);
s_cpp.Parse(tempArgs);

View file

@ -967,7 +967,12 @@ namespace System.Management.Automation
strongName, moduleName, psVersion, assemblyVersion, types, formats, null,
s_coreSnapin.Description, s_coreSnapin.DescriptionIndirect, null, null,
s_coreSnapin.VendorIndirect, null);
#if !UNIX
// NOTE: On Unix, logging has to be deferred until after command-line parsing
// complete. On Windows, deferring the call is not needed
// and this is in the startup code path.
SetSnapInLoggingInformation(coreMshSnapin);
#endif
return coreMshSnapin;
}

View file

@ -158,7 +158,7 @@ Describe 'Basic SysLog tests on Linux' -Tag @('CI','RequireSudoOnUnix') {
}
}
Describe 'Basic os_log tests on MacOS' -Tag @('CI','RequireSudoOnUnix') {
Describe 'Basic os_log tests on MacOS' -Tag @('Feature','RequireSudoOnUnix') {
BeforeAll {
[bool] $IsSupportedEnvironment = $IsMacOS
[bool] $persistenceEnabled = $false
@ -201,34 +201,33 @@ Describe 'Basic os_log tests on MacOS' -Tag @('CI','RequireSudoOnUnix') {
}
}
## Logging seems broken on macOS 10.13. It works fine on macOS 10.12.6.
## Travis CI updated macOS to 10.13.3 (kernel 17.4) and the logging tests start to fail.
# It 'Verifies basic logging with no customizations' -Skip:(!$IsSupportedEnvironment) {
It 'Verifies basic logging with no customizations' -Pending {
It 'Verifies basic logging with no customizations' -Skip:(!$IsSupportedEnvironment) {
$configFile = WriteLogSettings -LogId $logId
& $powershell -NoProfile -SettingsFile $configFile -Command '$env:PSModulePath | out-null'
$testPid = & $powershell -NoProfile -SettingsFile $configFile -Command '$PID'
Export-PSOsLog -After $after -Verbose | Set-Content -Path $contentFile
$items = Get-PSOsLog -Path $contentFile -Id $logId -After $after -TotalCount 3 -Verbose
# Made tests more reliable
Start-Sleep -Milliseconds 500
Export-PSOsLog -After $after -LogPid $testPid -Verbose | Set-Content -Path $contentFile
$items = @(Get-PSOsLog -Path $contentFile -Id $logId -After $after -TotalCount 3 -Verbose)
$items | Should -Not -Be $null
$items.Length | Should -BeGreaterThan 1
$items.Count | Should -BeGreaterThan 1
$items[0].EventId | Should -BeExactly 'Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational'
$items[1].EventId | Should -BeExactly 'Perftrack_ConsoleStartupStop:PowershellConsoleStartup.WinStop.Informational'
# if there are more items than expected...
if ($items.Length -gt 2)
if ($items.Count -gt 2)
{
# Force reporting of the first unexpected item to help diagnosis
$items[2] | Should -Be $null
}
}
# It 'Verifies logging level filtering works' -Skip:(!$IsSupportedEnvironment) {
It 'Verifies logging level filtering works' -Pending {
It 'Verifies logging level filtering works' -Skip:(!$IsSupportedEnvironment) {
$configFile = WriteLogSettings -LogId $logId -LogLevel Warning
& $powershell -NoProfile -SettingsFile $configFile -Command '$env:PSModulePath | out-null'
$testPid = & $powershell -NoLogo -NoProfile -SettingsFile $configFile -Command '$PID'
Export-PSOsLog -After $after -Verbose | Set-Content -Path $contentFile
Export-PSOsLog -After $after -LogPid $testPid -Verbose | Set-Content -Path $contentFile
# by default, powershell startup should only logs informational events.
# With Level = Warning, nothing should be logged.
$items = Get-PSOsLog -Path $contentFile -Id $logId -After $after -TotalCount 3

View file

@ -142,22 +142,34 @@ enum SysLogIds
# Defines the array indices when calling
# String.Split on an OsLog log entry
enum OsLogIds
Class OsLogIds
{
Date = 0;
Time = 1;
Thread = 2;
Type = 3;
Activity = 4;
Pid = 5;
ProcessName = 6;
Module = 7;
Id = 8;
CommitId = 9;
EventId = 10;
Message = 11;
[int] $Date = 0;
[int] $Time = 1;
[int] $Thread = 2;
[int] $Type = 3;
[int] $Activity = 4;
[int] $Pid = 5;
[int] $TTL = 6;
[int] $ProcessName = 7;
[int] $Module = 8;
[int] $Id = 9;
[int] $CommitId = 10;
[int] $EventId = 11;
[int] $Message = 12;
[void] UseOldIds()
{
$this.ProcessName=6;
$this.Module =7;
$this.Id =8;
$this.CommitId=9;
$this.EventId=10;
$this.Message=11;
}
}
class PSLogItem
{
[string] $LogId = [string]::Empty
@ -336,12 +348,13 @@ class PSLogItem
3: Type Default
4: activity 0x12
5: PID 39437
6: processname pwsh:
7: sourcedll (libpsl-native.dylib)
8: log source [com.microsoft.powershell.powershell]
9: commitid:treadid:channel (v6.0.1:1:10)
10:[EventId] [Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational]
11:Message Text
6: TTL (introduced in ~ 10.13) 0
7: processname pwsh:
8: sourcedll (libpsl-native.dylib)
9: log source [com.microsoft.powershell.powershell]
10: commitid:treadid:channel (v6.0.1:1:10)
11:[EventId] [Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational]
12:Message Text
#>
[object] $result = $content
@ -382,17 +395,28 @@ class PSLogItem
$item.Count = 1
$item.Timestamp = $time
$item.ProcessId = [int]::Parse($parts[[OsLogIds]::Pid])
$item.Message = $parts[[OsLogIds]::Message]
$osLogIds = [OsLogIds]::new();
$item.ProcessId = [int]::Parse($parts[$osLogIds.Pid])
# Around macOS 13, Apple added a field
# Detect if the field is the old or new field and if it is old
# Switch to the old schema
if($parts[$osLogIds.TTL] -match '\:')
{
$osLogIds.UseOldIds()
}
$item.Message = $parts[$osLogIds.Message]
# [com.microsoft.powershell.logid]
$splitChars = ('[', '.', ']')
$item.LogId = $parts[[OsLogIds]::Id]
$item.LogId = $parts[$osLogIds.Id]
$subparts = $item.LogId.Split($splitChars, [StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Length -eq 4)
{
$item.LogId = $subparts[3]
if ($id -ne $null -and $id -ne $item.LogId)
if ($null -ne $id -and $id -ne $item.LogId)
{
# this is not the log id we're looking for.
$result = $null
@ -401,7 +425,7 @@ class PSLogItem
}
# (commitid:TID:ChannelID)
$splitChars = ('(', ')', ':', ' ')
$item.CommitId = $parts[[OsLogIds]::CommitId]
$item.CommitId = $parts[$osLogIds.CommitId]
$subparts = $item.CommitId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Count -eq 3)
{
@ -416,7 +440,7 @@ class PSLogItem
# [EventId]
$splitChars = ('[', ']', ' ')
$item.EventId = $parts[[OsLogIds]::EventId]
$item.EventId = $parts[$osLogIds.EventId]
$subparts = $item.EventId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Count -eq 1)
{
@ -805,22 +829,41 @@ function Export-PSOsLog
[ValidateNotNullOrEmpty()]
[DateTime] $After,
[string] $LogId = "powershell"
[string] $LogId = "powershell",
[int] $LogPid
)
Test-MacOS
# NOTE: The use of double quotes and single quotes for the predicate parameter
# is mandatory. Reversing the usage (e.g., single quotes around double quotes)
# causes the double quotes to be stripped breaking the predicate syntax expected
# by log show
$extraParams = @()
if($LogPid)
{
$extraParams += @(
'--predicate'
"processID == $LogPid"
)
}
if ($After -ne $null)
{
[string] $startTime = $After.ToString("yyyy-MM-dd HH:mm:ss")
Start-NativeExecution -command {log show --info --start "$startTime" --predicate "subsystem == 'com.microsoft.powershell'"}
$extraParams += @(
'--start'
"$startTime"
)
}
else
{
Start-NativeExecution -command {log show --info --predicate "process == 'pwsh'"}
else {
$extraParams += @(
'--predicate'
"process == 'pwsh'"
)
}
Start-NativeExecution -command {log show --info @extraParams}
}
<#