Skip to content

Conversation

@SteveL-MSFT
Copy link
Member

@SteveL-MSFT SteveL-MSFT commented Nov 28, 2022

PR Summary

The path to the system wide powershell.config.json file depends on the location of SMA.dll. However, for some uses like hosting in a Windows Service, the .NET call to get the path to SMA.dll returns null which then subsequently fails a Path.Join() call. The change is to use AppContext.BaseDirectory instead of location of SMA.dll. In addition, the internal method takes a parameter that hasn't been used and has been removed.

I originally tried making the members nullable, however, this brought in a cascade of other changes needed to make it work across multiple files and also adds more regression risk since the behavior changes being nullable. Instead, I opened for this current approach which is more straight forward.

Also, this change is needed to experiment with building a single exe pwsh where SMA.dll won't be found.

PR Context

Fix #18109

PR Checklist

@iSazonov
Copy link
Collaborator

Perhaps we could use RuntimeType instead of Type in

internal static string GetApplicationBase(string shellId)
{
// Use the location of SMA.dll as the application base.
Assembly assembly = typeof(PSObject).Assembly;
return Path.GetDirectoryName(assembly.Location);
}

@SteveL-MSFT
Copy link
Member Author

@iSazonov RuntimeType is internal, can you explain how you think it could be used here?

@iSazonov
Copy link
Collaborator

@iSazonov RuntimeType is internal, can you explain how you think it could be used here?

I mean something like "123".GetType().Assembly.

@SteveL-MSFT
Copy link
Member Author

In this case, I don't think the result will be any different. The fundamental problem seems to be that in some hosted scenarios, SMA.dll's location is empty.

@iSazonov
Copy link
Collaborator

Maybe @jborean93 and @SeeminglyScience have ideas.

@daxian-dbw
Copy link
Member

daxian-dbw commented Nov 30, 2022

I think we need to fix GetApplicationBase instead.
This value of Utils.DefaultPowerShellAppBase is assumed to be always available and is depended at many other places, where A null value may cause problems that are unhandled.

We need to try reproducing this in a Windows Background Service to better understand the problem, and then contact the .NET team to see how to get the assembly location in that environment.

@iSazonov
Copy link
Collaborator

iSazonov commented Dec 2, 2022

Perhaps it is more reliable to use AppContext.BaseDirectory. See implementation https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/AppContext.cs,22

From https://learn.microsoft.com/en-us/dotnet/api/system.appcontext.basedirectory?view=net-7.0#remarks

In .NET 5 and later versions, for bundled assemblies, the value returned is the containing directory of the host executable.

Gets the file path of the base directory that the assembly resolver uses to probe for assemblies.

@SteveL-MSFT
Copy link
Member Author

@daxian-dbw I don't think we can rely on SMA.dll location as if we can get singleexe working, there isn't a SMA.dll as it's part of the exe. Perhaps @iSazonov's suggestion on base exe location is a better all around solution as long as hosted scenarios keep PowerShell assemblies in the same location as the host exe (which might not always be the case).

@iSazonov
Copy link
Collaborator

iSazonov commented Dec 2, 2022

Perhaps @iSazonov's suggestion on base exe location is a better all around solution as long as hosted scenarios keep PowerShell assemblies in the same location as the host exe (which might not always be the case).

To be clear - AppContext.BaseDirectory defaults to host exe location but it can be configured by the host to point a place where managed dll-s are.

@daxian-dbw
Copy link
Member

For an application that hosts PowerShell and build using dotnet publish, the PowerShell assemblies are under the publish\runtimes\win\lib\netx.x folder, while AppContext.BaseDirectory points to publish folder.

In the singleexe scenario, does typeof(PSObject).Assembly returns S.M.A.dll or the single exe binary that is produced from the build? If it's the latter, GetApplicationBase should continue to work unless the Assembly.Location always returns null for singleexe.

Also, does AppContext.BaseDirectory work in seingleexe? It's also using Assembly.Location, but just the entry assembly.

At any rates, simply handling the null case in PSConfiguration won't work, and we would need a more fundamental change to get the $PSHome value.

@SteveL-MSFT
Copy link
Member Author

SteveL-MSFT commented Dec 2, 2022

I have a branch that can build singleexe I can test this on. Note that singleexe currently is impossible to debug other than writing traces to the console.

Here's the results:

Assembly.FullName = System.Management.Automation, Version=7.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
Assembly.Location = 
AppContext.BaseDirectory = /Users/steve/repos/PowerShell/src/powershell-unix/bin/Debug/net7.0/osx-arm64/publish/

So it seems that AppContext.BaseDirectory may be the better fix here and we can document for host developers what they need to do if they don't have SMA.dll in the same folder as their host.

@daxian-dbw
Copy link
Member

daxian-dbw commented Dec 2, 2022

Looks good. Let's change to use AppContext.BaseDirectory then, and I can submit a PR to update the SDK NuGet package to binplace the module folder under publish instead of publish\runtimes\win\lib\netx.x as of today.

The only thing is we cannot verify whether AppContext.BaseDirectory works in Windows Background Service.

@SteveL-MSFT
Copy link
Member Author

So SingleExe build does work as long as you don't use trimming. But the result is a 112MB executable.

I think for a Windows Service, we'll have to wait for user feedback.

@SteveL-MSFT SteveL-MSFT changed the title Handle case where system wide config path cannot be found Change detection of home directory to the host instead of SMA.dll location Dec 2, 2022
Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

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

@SteveL-MSFT Can you please verify what AppContext.BaseDirectory returns in a remote session?

I found the following comments in https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs#LL628-L630C61

   // However, 'Assembly.GetEntryAssembly()' returns null when the managed code is called from
   // unmanaged code (PowerShell WSMan remoting scenario), so in that case, we continue to use
   // the location of 'System.Management.Automation.dll'.

Since AppContext.BaseDirectory uses Assembly.GetEntryAssembly(), I'm afraid it may not work a remote session.

@ghost ghost added the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Dec 2, 2022
@ghost ghost removed the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Dec 2, 2022
@SteveL-MSFT
Copy link
Member Author

@daxian-dbw unfortunately I don't have a system where I can do WinRM remoting (it doesn't work on win-arm64 due to the msix install sandbox)

@SteveL-MSFT
Copy link
Member Author

@SteveL-MSFT
Copy link
Member Author

What if we use AppContext.BaseDirectory only as a fallback?

@SteveL-MSFT SteveL-MSFT changed the title Change detection of home directory to the host instead of SMA.dll location Change detection of home directory to fallback to host if SMA.dll location is not found Dec 3, 2022
@SteveL-MSFT SteveL-MSFT reopened this Mar 13, 2023
@ghost ghost removed the Review - Needed The PR is being reviewed label Mar 13, 2023
@pull-request-quantifier-deprecated

This PR has 28 quantified lines of changes. In general, a change size of upto 200 lines is ideal for the best PR experience!


Quantification details

Label      : Extra Small
Size       : +15 -13
Percentile : 11.2%

Total files changed: 7

Change summary by file extension:
.cs : +15 -13

Change counts above are quantified counts, based on the PullRequestQuantifier customizations.

Why proper sizing of changes matters

Optimal pull request sizes drive a better predictable PR flow as they strike a
balance between between PR complexity and PR review overhead. PRs within the
optimal size (typical small, or medium sized PRs) mean:

  • Fast and predictable releases to production:
    • Optimal size changes are more likely to be reviewed faster with fewer
      iterations.
    • Similarity in low PR complexity drives similar review times.
  • Review quality is likely higher as complexity is lower:
    • Bugs are more likely to be detected.
    • Code inconsistencies are more likely to be detected.
  • Knowledge sharing is improved within the participants:
    • Small portions can be assimilated better.
  • Better engineering practices are exercised:
    • Solving big problems by dividing them in well contained, smaller problems.
    • Exercising separation of concerns within the code changes.

What can I do to optimize my changes

  • Use the PullRequestQuantifier to quantify your PR accurately
    • Create a context profile for your repo using the context generator
    • Exclude files that are not necessary to be reviewed or do not increase the review complexity. Example: Autogenerated code, docs, project IDE setting files, binaries, etc. Check out the Excluded section from your prquantifier.yaml context profile.
    • Understand your typical change complexity, drive towards the desired complexity by adjusting the label mapping in your prquantifier.yaml context profile.
    • Only use the labels that matter to you, see context specification to customize your prquantifier.yaml context profile.
  • Change your engineering behaviors
    • For PRs that fall outside of the desired spectrum, review the details and check if:
      • Your PR could be split in smaller, self-contained PRs instead
      • Your PR only solves one particular issue. (For example, don't refactor and code new features in the same PR).

How to interpret the change counts in git diff output

  • One line was added: +1 -0
  • One line was deleted: +0 -1
  • One line was modified: +1 -1 (git diff doesn't know about modified, it will
    interpret that line like one addition plus one deletion)
  • Change percentiles: Change characteristics (addition, deletion, modification)
    of this PR in relation to all other PRs within the repository.


Was this comment helpful? 👍  :ok_hand:  :thumbsdown: (Email)
Customize PullRequestQuantifier for this repository.

Assembly assembly = typeof(PSObject).Assembly;
return Path.GetDirectoryName(assembly.Location);
// AppContext is needed where PS7 may be compiled as a single exe
// Environment.ProcessPath covers the general case
Copy link
Member

Choose a reason for hiding this comment

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

What is the general case that Environment.ProcessPath is going to cover?
AppContext.BaseDirectory calls Environment.ProcessPath in case of NativeAOT, so I think that already covers it, isn't it?

Comment on lines +487 to +494
if (!string.IsNullOrEmpty(AppContext.BaseDirectory))
{
return Path.TrimEndingDirectorySeparator(AppContext.BaseDirectory);
}
else if (!string.IsNullOrEmpty(Path.GetDirectoryName(typeof(PSObject).Assembly.Location)))
{
return Path.GetDirectoryName(typeof(PSObject).Assembly.Location);
}
Copy link
Member

Choose a reason for hiding this comment

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

We should use a static variable to hold the resulted application base path, instead of running this every time.
Also, users can change the application path by AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", <new-path>), which may completely break PowerShell, so we have to cache the original value on the first call to GetApplicationBase.

#endif

internal static string DefaultPowerShellAppBase => GetApplicationBase(DefaultPowerShellShellID);
internal static string DefaultPowerShellAppBase => GetApplicationBase();
Copy link
Member

Choose a reason for hiding this comment

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

The AddType.cs needs to be updated too:

private static readonly string s_netcoreAppRefFolder = PathType.Combine(
PathType.GetDirectoryName(
(Assembly.GetEntryAssembly() ?? typeof(PSObject).Assembly).Location),
"ref");
// Path to the folder where .NET Core runtime assemblies are located.
private static readonly string s_frameworkFolder = PathType.GetDirectoryName(typeof(object).Assembly.Location);

s_netcoreAppRefFolder should be just using GetApplicationBase(). As for s_frameworkFolder, the reference of this variable can be replaced by GetApplicationBase() directly I think.

}
#endif

internal static string DefaultPowerShellAppBase => GetApplicationBase(DefaultPowerShellShellID);
Copy link
Member

Choose a reason for hiding this comment

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

I guess we can remove all the reference to DefaultPowerShellAppBase now, and just GetApplicationBase() directly instead.

{
var results = new List<CompletionResult>();
string userHelpDir = HelpUtils.GetUserHomeHelpSearchPath();
string appHelpDir = Utils.GetApplicationBase(Utils.DefaultPowerShellShellID);
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove Utils.DefaultPowerShellID now?

@ghost ghost added the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Mar 20, 2023
@daxian-dbw
Copy link
Member

This change will cause a problem to the Microsoft.PowerShell.SDK package., so adding @adityapatwardhan for input.

Today, $PSHome points to the folder where S.M.A.dll is located, so for a self-contained application that hosts PowerShell, the $PSHome points to different locations when it's running on Windows vs. on Unix (e.g. publish\runtimes\win\lib\net7.0 vs. publish\runtimes\unix\lib\net7.0).

The way we ship the built-in modules in Microsoft.PowerShell.SDK leveraged that -- when running dotnet publish, the Windows version built-in modules will be bin-placed to runtimes\win\lib\net7.0\Modules, and the Unix version of built-in modules will be bin-placed to runtimes\unix\lib\net7.0\Modules. As a result, the application will have the expected modules in its $PSHome\Modules module path.

After this change, the $PSHome will points to the same location for the application (i.e. the publish folder), but we cannot bin-place both the Windows version and Unix version of built-in modules to the same location publish\Modules. This will break the PowerShell NuGet package, which is depended by Azure Function and .NET Interactive.

@ghost ghost added the Stale label Apr 4, 2023
@ghost
Copy link

ghost commented Apr 4, 2023

This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 15 days. It will be closed if no further activity occurs within 10 days of this comment.

@ghost ghost closed this Apr 15, 2023
@daxian-dbw daxian-dbw reopened this Apr 17, 2023
@ghost ghost closed this Apr 27, 2023
@daxian-dbw daxian-dbw reopened this Apr 27, 2023
@daxian-dbw daxian-dbw removed the Stale label Apr 27, 2023
@daxian-dbw
Copy link
Member

@adityapatwardhan Please take a look at #18674 (comment). We may need to brainstorm on this NuGet packaging issue.

@daxian-dbw daxian-dbw added WG-Engine core PowerShell engine, interpreter, and runtime CL-Engine Indicates that a PR should be marked as an engine change in the Change Log Needs-Triage The issue is new and needs to be triaged by a work group. labels May 1, 2023
@ghost ghost added the Stale label May 16, 2023
@ghost
Copy link

ghost commented May 16, 2023

This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 15 days. It will be closed if no further activity occurs within 10 days of this comment.

@ghost ghost closed this May 27, 2023
@SCLDGit
Copy link

SCLDGit commented Sep 27, 2023

This is a huge thorn in our side. As long as there are vital configuration functions which are only available via PowerShell (such as the enabling/disabling of server roles and features), which our application must be able to call via C#, this is going to continue to be a headache for us and our deployment requirements. The .NET Core 3.1 version of single file deployment (extract all files to /temp) is simply not acceptable to our customer base for security reasons.

This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log Extra Small Needs-Triage The issue is new and needs to be triaged by a work group. Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept WG-Engine core PowerShell engine, interpreter, and runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The type initializer for 'System.Management.Automation.ExperimentalFeature' threw an exception.

5 participants