-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow users that have certain special characters in their username to build successfully when using exec #6223
Changes from 12 commits
1314a77
9084023
19c9e4c
94dde5c
7ce00b3
95fca99
34126e1
1c5a2b3
c268b32
7b58784
803cadf
aff3425
6d07c7d
e2d8c0c
828dedc
11c0021
73e8c5c
9cca2db
e05b2e1
ef369fb
ee79111
674dccd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,46 @@ private ExecWrapper PrepareExecWrapper(string command) | |
return exec; | ||
} | ||
|
||
[Fact] | ||
[Trait("Category", "mono-osx-failing")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this failing on other OSes? Does it need fixing there too? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but since we don't currently have a customer asking for it, BenVillalobos wanted to hold off on fixing it until we have feedback that it's needed. |
||
[Trait("Category", "netcore-osx-failing")] | ||
[Trait("Category", "netcore-linux-failing")] | ||
public void EscapeSpecifiedCharactersInPathToGeneratedBatchFile() | ||
{ | ||
using (var testEnvironment = TestEnvironment.Create()) | ||
{ | ||
var newTempPath = testEnvironment.CreateNewTempPathWithSubfolder("hello()w]o(rld)").TempPath; | ||
|
||
string tempPath = Path.GetTempPath(); | ||
Assert.StartsWith(newTempPath, tempPath); | ||
|
||
// Now run the Exec task on a simple command. | ||
Exec exec = PrepareExec("echo Hello World!"); | ||
exec.CharactersToEscape = "()]"; | ||
exec.Execute().ShouldBeTrue(); | ||
} | ||
} | ||
|
||
[Fact] | ||
[Trait("Category", "mono-osx-failing")] | ||
[Trait("Category", "netcore-osx-failing")] | ||
[Trait("Category", "netcore-linux-failing")] | ||
public void EscapeParenthesesInPathToGeneratedBatchFile_DuplicateCharactersToEscapeDontGetEscapedMultipleTimes() | ||
{ | ||
using (var testEnvironment = TestEnvironment.Create()) | ||
{ | ||
var newTempPath = testEnvironment.CreateNewTempPathWithSubfolder("hello()wo(rld)").TempPath; | ||
|
||
string tempPath = Path.GetTempPath(); | ||
Assert.StartsWith(newTempPath, tempPath); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: no newline |
||
// Now run the Exec task on a simple command. | ||
Exec exec = PrepareExec("echo Hello World!"); | ||
exec.CharactersToEscape = "()()"; | ||
exec.Execute().ShouldBeTrue(); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Ensures that calling the Exec task does not leave any extra TEMP files | ||
/// lying around. | ||
|
@@ -918,6 +958,59 @@ public void EndToEndMultilineExec() | |
} | ||
} | ||
} | ||
|
||
[Fact] | ||
[Trait("Category", "mono-osx-failing")] | ||
[Trait("Category", "netcore-osx-failing")] | ||
[Trait("Category", "netcore-linux-failing")] | ||
public void EndToEndMultilineExec_WithCharactersToEscapeMetadata() | ||
{ | ||
using (var env = TestEnvironment.Create(_output)) | ||
{ | ||
var testProject = env.CreateTestProjectWithFiles(@"<Project> | ||
<Target Name=""ExecCommand""> | ||
<Exec CharactersToEscape=""()"" Command=""echo Hello, World!"" /> | ||
</Target> | ||
</Project>"); | ||
|
||
// Ensure path has subfolders | ||
var newTempPath = env.CreateNewTempPathWithSubfolder("hello()wo(rld)").TempPath; | ||
string tempPath = Path.GetTempPath(); | ||
Assert.StartsWith(newTempPath, tempPath); | ||
|
||
using (var buildManager = new BuildManager()) | ||
{ | ||
MockLogger logger = new MockLogger(_output, profileEvaluation: false, printEventsToStdout: false); | ||
|
||
var parameters = new BuildParameters() | ||
{ | ||
Loggers = new[] { logger }, | ||
}; | ||
|
||
var collection = new ProjectCollection( | ||
new Dictionary<string, string>(), | ||
new[] { logger }, | ||
remoteLoggers: null, | ||
ToolsetDefinitionLocations.Default, | ||
maxNodeCount: 1, | ||
onlyLogCriticalEvents: false, | ||
loadProjectsReadOnly: true); | ||
|
||
var project = collection.LoadProject(testProject.ProjectFile).CreateProjectInstance(); | ||
|
||
var request = new BuildRequestData( | ||
project, | ||
targetsToBuild: new[] { "ExecCommand" }, | ||
hostServices: null); | ||
|
||
var result = buildManager.Build(parameters, request); | ||
|
||
logger.AssertLogContains("Hello, World!"); | ||
|
||
result.OverallResult.ShouldBe(BuildResultCode.Success); | ||
} | ||
} | ||
} | ||
} | ||
|
||
internal class ExecWrapper : Exec | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -188,6 +188,19 @@ public ITaskItem[] Outputs | |
[Output] | ||
public ITaskItem[] ConsoleOutput => !ConsoleToMSBuild ? Array.Empty<ITaskItem>(): _nonEmptyOutput.ToArray(); | ||
|
||
private HashSet<char> _charactersToEscape; | ||
|
||
public string CharactersToEscape | ||
{ | ||
set | ||
{ | ||
if (!string.IsNullOrEmpty(value)) | ||
{ | ||
_charactersToEscape = new HashSet<char>(value); | ||
} | ||
} | ||
} | ||
|
||
#endregion | ||
|
||
#region Methods | ||
|
@@ -611,18 +624,38 @@ protected internal override void AddCommandLineCommands(CommandLineBuilderExtens | |
|
||
// If for some crazy reason the path has a & character and a space in it | ||
// then get the short path of the temp path, which should not have spaces in it | ||
// and then escape the & | ||
if (batchFileForCommandLine.Contains("&") && !batchFileForCommandLine.Contains("^&")) | ||
{ | ||
batchFileForCommandLine = NativeMethodsShared.GetShortFilePath(batchFileForCommandLine); | ||
batchFileForCommandLine = batchFileForCommandLine.Replace("&", "^&"); | ||
} | ||
|
||
StringBuilder fileName = StringBuilderCache.Acquire(batchFileForCommandLine.Length); | ||
|
||
// Escape any characters specified by the CharactersToEscape metadata, or '&' | ||
for (int i = 0; i < batchFileForCommandLine.Length; i++) | ||
{ | ||
char c = batchFileForCommandLine[i]; | ||
|
||
if (ShouldEscapeCharacter(c) && (i == 0 || batchFileForCommandLine[i - 1] != '^')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You said you wanted to put off fixing edge cases like not escaping a character preceded by an escaped ^, correct? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. at the moment we've only gotten a request for the parentheses case (user's username was something(dev) which seems reasonable). So this fix will account for most other special characters, and I'll make an issue tracking that we don't account for '^''s that are already escaped and the other special cases that arise with it. If it gains traction we can worry about it then. For now this is a "better than it was before" PR. |
||
{ | ||
fileName.Append('^'); | ||
} | ||
fileName.Append(c); | ||
} | ||
|
||
batchFileForCommandLine = StringBuilderCache.GetStringAndRelease(fileName); | ||
} | ||
|
||
commandLine.AppendFileNameIfNotNull(batchFileForCommandLine); | ||
} | ||
} | ||
|
||
private bool ShouldEscapeCharacter(char c) | ||
{ | ||
// Escape '&' to preserve previous functionality | ||
return c == '&' || (_charactersToEscape != null && _charactersToEscape.Contains(c)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking about performance here...this function should be fast, but the batch file could be huge, in which case this would be called an immense number of times. It would be nice to factor out things like the null check to make this particular function call even faster. If you want it even faster, you could make a bit array representing the characters to escape (i.e., bit x is 1 iff (int)c is in _charactersToEscape) and check that instead of this HashSet. On the other hand, I wouldn't be surprised if there's an optimization to HashSet to do just that, so it would be worth checking whether it makes a difference first. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point. In theory we could swap the implementation from: "Supply a set of the characters you want escaped" to "Escape all special characters (but if some are already escaped, or you fall into a specific edge case, you're out of luck)". If we have an array of the characters we know need to be escaped and the user sets "TheMagicFlag", we could iterate through the array and check Thoughts? /cc: @ladipro There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did some rough testing with stopwatches comparing hashset.contains with a for loop through an array of special characters that checks There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here I am on a weekend realizing "wait, why not just do a simple comparison?" I was incepted by the bit comment above 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, (c | specialChars[i] == c) would actually be wrong if, for example, specialChars[i] were 0, it would always return true. |
||
} | ||
|
||
#endregion | ||
|
||
#region Overridden properties | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While we're here, this was always bugging me. Should we have a root folder "MSBuild" inside TEMP, and stick all our stuff inside that? Otherwise you can't attribute to which process a given file belongs to. Just being good citizens of TEMP. Maybe replace Temporary with MSBuild (and have a subfolder).
See #6219
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might that trip some customers into a path-too-long situation?