Skip to content
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

Add ability to mark self-contained .exe as Windows GUI program. #2470

Merged
merged 7 commits into from
Aug 21, 2018

Conversation

vitek-karas
Copy link
Member

On Windows only, if the OutputType of the project is WinExe (as oppose to just exe), self-contained published app will now have its subsystem set to Windows GUI.
This means the app will start without a console.

The change renames the task EmbedAppNameInHost to PrepareAppHost as it now does more than just embedding the path to the app.

Added unit tests for the AppHost class and then an E2E for the publish command.

@livarcocc livarcocc requested review from peterhuene and a team August 14, 2018 00:34
@livarcocc
Copy link
Contributor

@peterhuene does this conflict with your mode change?

@vitek-karas we are working on having exes for framework dependent apps as well. Should the same change be available there as well? If it is simply in your roadmap and will come in later, disregard my question.

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why all the formatting changes o this file?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VS started doing this at some point. I think we should just take it because I've reverted it myself a bunch of times. Would have preferred to do it separately, but let's get it over with.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #2471 with just the formatting changes - will wait for it to merge.

<comment>{StrBegin="NETSDK1071: "}</comment>
</data>
<data name="AppHostNotWindowsCLI" xml:space="preserve">
<value>NETSDK1072: Unable to use '{0}' as application host executable because it's not a Windows executable for teh CUI subsystem.</value>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a typo here. teh instead of the. Also, CUI system? Should it be GUI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed. CUI is the Windows term for console app subsystem. GUI - Graphical subsystem, CUI - Console subsystem. The apphost.exe should be prebuilt as a CUI, if not this check will fail (also there to make sure we didn't compute wrong index).
I added the (Console) into the error message to make this clearer.

@vitek-karas
Copy link
Member Author

I only just now learned that there are similar efforts... so if this somehow collides with other people's work, please do let me know. I made this change mostly as a learning experience... and since it works just fine, made it a PR.

This seems to work even for framework dependent exes. As it should - I would not expect any differences in this regard. I tried locally building an app with /p:UseAppHost=true /p:SelfContained=false /p:OutputType=WinExe and it did produce an .exe which has the GUI bit set but otherwise seemed to work as usual.

@vitek-karas
Copy link
Member Author

Fixes #2464

@vitek-karas
Copy link
Member Author

Related to dotnet/core-setup#196

@@ -401,4 +401,12 @@ The following are names of parameters or literal values and should not be transl
<value>NETSDK1070: The application configuration file must have root configuration element.</value>
<comment>{StrBegin="NETSDK1070: "}</comment>
</data>
</root>
<data name="AppHostNotWindows" xml:space="preserve">
<value>NETSDK1071: Unable to use '{0}' as application host executable because it's not a Windows PE file.</value>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll likely want to improve this UX if users are trying to build a WinExe output type project and specify a non-Windows RID, but that shouldn't prohibit merging this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the .targets will make it so that we effectively ignore WinExe on non-Windows RIDs. Just like we do today. I didn't want to break anything, so on Windows RIDs, we now honor the OutputType=Exe (CUI)/WinExe (GUI). On non-Windows RIDs both Exe/WinExe will behave just like now - that is the same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, with respect to this change this looks good; in the future I think we should error with something that informs the user that this project requires a Windows RID because the output type is WinExe.

@vitek-karas
Copy link
Member Author

@KathleenDollard for potential UX aspects.
In short, on Windows RIDs this change will make it so that OutputType=Exe produces Console apphost.exe and OutputType=WinExe produces GUI apphost.exe.

try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
Copy link
Contributor

@peterhuene peterhuene Aug 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have sanity bounds check that on the view handle that it is at least 0x99 bytes in length?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - will do. Will reuse the exception message that it's not a valid Windows PE file (since it wouldn't be :-))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

@peterhuene peterhuene Aug 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also couldn't math. Need two checks: one that the size is at least 0x40 and the other is that the NT header offset + 0x5C is within bounds.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely - already working on it

@@ -272,7 +272,7 @@ Copyright (c) .NET Foundation. All rights reserved.
Computes any files that need to be copied to the build output folder for .NET Core.
============================================================
-->
<UsingTask TaskName="EmbedAppNameInHost" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" />
<UsingTask TaskName="PrepareAppHost" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 on name change, as this will also make more sense for additional 3.0 changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this name has been bugging me for a while. :)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Although "Prepare" doesn't really mean too much. And the file is done after this target right? and it will just be copied around

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of anything better right now - if you have a better name, I'll be happy to change it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateAppHost might be good since it matches what we called it in the C# helper. I'm OK with Prepare too though.

@peterhuene
Copy link
Contributor

@livarcocc This will not conflict with the SDK changes for using the apphost for framework-dependent apps, since it's just some targets logic.

@vitek-karas Changes look good, other than the one comment for a bounds check where we're modifying the PE header. Thanks!

/// <summary>
/// Create an AppHost with embedded configuration of app binary location
/// </summary>
/// <param name="appHostSourceFilePath">The path of Apphost template, which has the place holder</param>
/// <param name="appHostDestinationFilePath">The destination path for desired location to place, including the file name</param>
/// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param>
/// <param name="overwriteExisting">If override the file existed in <paramref name="appHostDestinationFilePath"/></param>
/// <param name="customizationSettings">Optional settings to customize the creates apphost</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo: s/the creates apphost/the created/apphost

/// <summary>
/// Settings to customize the apphost.
/// </summary>
public class HostCustomizationSettings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think "Customization" is redundant when we're already talking about "Settings" or "Options". I also think "Host" is redundant in nested scope with AppHost.

Even more subjective, but I also prefer "Options" over "Settings".

And finally, Framework Design Guidelines discourage public nested classes. Those don't apply here, but I like to follow this one.

I would prefer any of these (listed in my personal order of preference):

  • AppHostOptions (top-level)
  • AppHostSettings (top-level)
  • AppHost.Options (nested)
  • AppHost.Settings (nested)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about making the AppHostOptions class separate (top-level), but then it would require its own file and seems way too small for that. But I will happily oblige if others would like that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind a new file since we're likely to keep expanding the options as the 3.0 features get implemented. I'd also second AppHostOptions.

public static void Create(
string appHostSourceFilePath,
string appHostDestinationFilePath,
string appBinaryFilePath,
bool overwriteExisting = false)
bool overwriteExisting = false,
HostCustomizationSettings customizationSettings = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing earlier nit, I'd rename parameter here to just settings. (Or justoptions if you do take the "Options" over "Settings" in the class name.)

@@ -272,7 +272,7 @@ Copyright (c) .NET Foundation. All rights reserved.
Computes any files that need to be copied to the build output folder for .NET Core.
============================================================
-->
<UsingTask TaskName="EmbedAppNameInHost" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" />
<UsingTask TaskName="PrepareAppHost" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this name has been bugging me for a while. :)


// https://en.wikipedia.org/wiki/Portable_Executable
// Validate that we're looking at Windows PE file
if (((UInt16*)bytes)[0] != 0x5A4D)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put these numbers into consts? They can be local and close by first use and keeping the comments to documentation nearby as well.

@nguerrera
Copy link
Contributor

Looks good overall, I only had nits.

/// <summary>
/// The first two bytes of a PE file are a constant signature.
/// </summary>
private const UInt16 _peFileSignature = 0x5A4D;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could we use const-naming from the guidelines? When reading the source, it makes me think these are fields.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely - I just copied what was already in the class (look at the top of the file).
Just to make sure - const field would be PascalCased right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, I didn't notice that we do it against the guideline already in this file. I'd still advocate for doing it the "correct" way here (PascalCased) :)

private const string _placeHolder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"; //hash value embedded in default apphost executable
private readonly static byte[] _bytesToSearch = Encoding.UTF8.GetBytes(_placeHolder);

[Fact]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WindowsOnlyFact] ? it used _windowsFileHeader

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry - I don't understand your comment.
The GUI bit only works on Windows RIDs. I guess it might be possible to run the publish on Mac for a Windows RID and it should still work... I'll have to try that somehow.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, it is not real file

@peterhuene
Copy link
Contributor

The test failures on Windows appear to be infrastructure-related. I'd rerun, but there might still be one more commit pushed if my remaining naming nit is fixed.


// This is a dump of first 350 bytes of a windows apphost.exe
// This includes the PE header and part of the Optional header
private static byte[] _windowsFileHeader = new byte[] {
private static byte[] WindowsFileHeader = new byte[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is technically not a constant. I would have used s_windowsFileHeader from my corefx upbringing, but we don't seem to be consistent on that and I think our editorconfig was tweaked to use just _ even for static fields.

I don't care enough about this to block the commit, though. Seems we should make a call and fix all the static fields up separately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented on #1589

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this should be const/readonly.. I'll fix that. In which case the new naming scheme fits much better.

@nguerrera
Copy link
Contributor

@dotnet-bot test this please

On Windows only, if the OutputType of the project is WinExe (as oppose to just exe), self-contained published app will now have its subsystem set to Windows GUI.
This means the app will start without a console.

The change renames the task EmbedAppNameInHost to PrepareAppHost as it now does more than just embedding the path to the app.

Added unit tests for the AppHost class and then an E2E for the publish command.
Can't rely on the optional header to stay at fixed offset, so read the offset from the PE header even in tests.
- Move the options into a top-level class AppHostOptions
- Use named constants for all the magic numbers
- Check for buffer overflows
- Fix typos
Use named constants in tests as well
Also added better description in the comment of the task.
Fix the naming in tests as well (product code has been changed in a previous commit).
Also makes one of the const fields readonly as it should not be mutable.
@vitek-karas
Copy link
Member Author

I renamed the task to CreateAppHost. Unless somebody complains, I'm going to merge this tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants