From bba0527af9853f984a30183c2d9d5bfca7469f09 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett (MSFT)" Date: Thu, 30 Jan 2020 20:17:21 -0800 Subject: [PATCH] Collect all known PowerShell Core installations for dynamic profiles (#4273) This pull request teaches the PowerShell Core generator about a bunch of different locations in which it might find a PowerShell. These instances will be sorted, a leader will be elected, and that leader will be promoted and given the vaunted title of "PowerShell". Names will be generated for the rest. The sort order is documented in the comments, but that comment will be replicated here: ``` // <-- Less Valued .................................... More Valued --> // | All instances of PS 6 | All PS7 | // | Preview | Stable | ~~~ | // | Non-Native | Native | Non-Native | Native | ~~~ | // | Trd | Pack | Trd | Pack | Trd | Pack | Trd | Pack | ~~~ | // (where Pack is a stand-in for store, scoop, dotnet, though they have their own orders, // and Trd is a stand-in for "Traditional" (Program Files)) ``` Closes #2300 --- .../ProfileIcons/pwsh-preview.scale-100.png | Bin 0 -> 467 bytes .../ProfileIcons/pwsh-preview.scale-125.png | Bin 0 -> 606 bytes .../ProfileIcons/pwsh-preview.scale-150.png | Bin 0 -> 680 bytes .../ProfileIcons/pwsh-preview.scale-200.png | Bin 0 -> 809 bytes .../ProfileIcons/pwsh.scale-100.png | Bin 0 -> 750 bytes .../ProfileIcons/pwsh.scale-125.png | Bin 0 -> 1051 bytes .../ProfileIcons/pwsh.scale-150.png | Bin 0 -> 989 bytes .../ProfileIcons/pwsh.scale-200.png | Bin 0 -> 1357 bytes .../PowershellCoreProfileGenerator.cpp | 341 +++++++++++++++--- .../PowershellCoreProfileGenerator.h | 4 - src/cascadia/TerminalApp/Profile.h | 1 + 11 files changed, 291 insertions(+), 55 deletions(-) create mode 100644 src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-100.png create mode 100644 src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-125.png create mode 100644 src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-150.png create mode 100644 src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-200.png create mode 100644 src/cascadia/CascadiaPackage/ProfileIcons/pwsh.scale-100.png create mode 100644 src/cascadia/CascadiaPackage/ProfileIcons/pwsh.scale-125.png create mode 100644 src/cascadia/CascadiaPackage/ProfileIcons/pwsh.scale-150.png create mode 100644 src/cascadia/CascadiaPackage/ProfileIcons/pwsh.scale-200.png diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-100.png b/src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-100.png new file mode 100644 index 0000000000000000000000000000000000000000..de883b838bfcbc904b05b56784adf4c40b9f59f6 GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMfr2wB0S0Md{p=U2!Bapnu)CnY! z(KfYNEA{8jR++M|3bVRi4OTj*%g0n2G>p1G%MA8cQ3l6h3zX)9S|NsA+i_i9R zm+xR}XyGlf)UON>&e*m5n5|lolWdN9@I|bOGHsK5%DnUMzPfC>IK_7H zBgQTrukQZq&p&?sskZodrbF|Ur|*wEcq2ad2>;BZ(L&iT6Ssc;@U>#kRi|xN9Clqd z*mBvrYFChQ!Sko@LbK+dxcRIsXmwr4FS$5Q)pJ=QjE=If}SF{Ct1zPG%mqq(^QXoF!!?d-nTp+8`2e zBVq-&SDH@Fn;*L_%kFSr@Mk}(*;JLk7As5^%?zEM7wCMT)NIYYpjE=<+BfgEJp1Xi z&yRJ%J&%|~lm8qH2Xp33?@X`HmwjLmA^H9O>L1&B;t%8;c=f1o^10bl=0!=cu6c9) z_?Js>cI!FmtkBK5+PZax?Q;Jw0gk2L&HqL1d@ru=tRtthCwVu}y$qhNelF{r5}E+N C7Sx;o literal 0 HcmV?d00001 diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-125.png b/src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-125.png new file mode 100644 index 0000000000000000000000000000000000000000..329fc8e92e7d7d6d204d6ded15f9e7f88bd6e7e3 GIT binary patch literal 606 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqMyzVB`z%32_B--!SyR(E-*b2>U)$ zCtUCb-^2^dZ6_qB-ev0C!Pej=k`^GGQNUjiEtGwjwYiF~sD-!WI&=HChGQQL76Enj zbC<8(#}0b-GEASRT4d|{KZALuX850-TAMi^ zHS(Q~JP~submsa>?zY~Q?Yj&aEl-}kwn#@;`I*x0O|!yR9f(nEip^eMZY3SNJMu_g zn)pK9m$|nUtuvO_wB6oXy=ndl%@Z#hOilTIDYZM7i_DkI;(M_}y?L@q#Vd_ao)>5A z#dq%TnX^|*V`~1zue__a|JCSvr;vU@Pw?*P!^z)04{YA5Z+Su|Z2!K%+xwdNv+a#1 ql=7SGUH{&`?$XDZ$`;ABf5jPORp&GO6>|i7ox#)9&t;ucLK6Tls2rRC literal 0 HcmV?d00001 diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-150.png b/src/cascadia/CascadiaPackage/ProfileIcons/pwsh-preview.scale-150.png new file mode 100644 index 0000000000000000000000000000000000000000..2e115fdff4dbf7693e83b87f92b6665d6779e31b GIT binary patch literal 680 zcmV;Z0$2TsP);R0?1BS*0gX{o|>;R1H0F3MajO+l6>;R0dE1c{AjO+l6zy*TX4w1+i zmCY8E!v%v|5^F{jW^fO1nF@L00gCp3#`llS>;R0{1Bb2%eozx=|NsBlqs@8_a-#`* z%>;#n4RlB)b&m^oql&Y-r^aOvZ7@NFKXJ4*IDZmO(RDWAnGguaSgn2WsC1e?Q;*XJUu%~@u$=jicnUzZk1R@y36#DbkHNkUiLoi0nL8GD0000KbW%=J06_2WpU=+_P+)){zwfVb z@4wIQr}`9&0003aNklXf_hIIo`{m?LGM3d>B?i<#B?Y8x0Efj_B->E}2Ei>)Od_JE9kG2^`)T$wS|buo zIn$LH^i)N8voM!pkX4BD3q)0^=8iRh`eKej9?=D;G6{k}V7+0M3Jiu2eF(|2i8Uw?C5i)f?kZ9h)&Nsk#9!XFYr_K`32GCRcxE;f9EA@9z1e)&qmbtZdMi5uTo3Oj7{8 zGI+}{Nb`%@?;r4KI$t7bH742N3_e28OEBNEK`R;l3#y;rp2lU0KI#vK>o|@T1IyR| O0000G|hb3b=^0e?Y&aK;CNMZfkx`rdzb9Y@{I^|$~3|9^Aw z+3U>hXIWZT3bf3Rn!9WHF{#`o=S8M;=gha%ujK96Cf9$ktzw0NW2 zw7UQL^V%&J*ry-ds5#qY_1W|9zCL;L*>>(BruoP2a!vUB;p?iCcO_>Y)M?pwL#XfL z*ProAkFm@?oF21oqxs~&_di~H^hS63{({pF8!kQ(Sb1iO?cyNig4uKT^LFoW*mb?_ z=$$X0e`Kf4b`{TvU3s!NsBhKOeS1}>a8B9R^C;PS&^dd(k)rNuC6SMs(Ar-svqC0{`24df$t@z zb?@)#*WUX4?B0_(S04Y=f7-f#mGg^z6-|HQxHt~17n*b>XxF;cFH>gctZ;Mi3-FQM zwX53Y@{Uvw%f!euD-8yNM+|$Xe3NWTyUDO^Rc6+)Fpih}g|<2AA09I-EeTi?Vq(XW zI!TtRSfQmick+{%Lu((_IKDINV3=>+al~U|h*tlj(-9jF-PjS%z{n7*^Y92u_G(Z2 z>&iD0yv*kQb360HLZDRbL4aAe%T`tgb@rrP%so#dc^h{wv(xW==OBKhLud6h!H5i| zZo_ruOdAzit6Fsz`b-a>bs#H5WvU`4!{iBDOQ)EdZI6n+(YM1ha>2wqe6My_=ceB% zpX^lru%Wu3;z~yO!yBGm_gg0iUpas7=Fz1qv>9WpJr~v;zJ34R#%itCjtqW5V(c2V zHC5|2M8z`c$COX@D&~&6f6q54Y0XDPuQ^A2y>`uUFz)X8>vM{^>kQ}or}|Zix7B<9 heLhzG|7`qE&RZ|S|3+qUhyr7b!PC{xWt~$(698RggQoxh literal 0 HcmV?d00001 diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/pwsh.scale-100.png b/src/cascadia/CascadiaPackage/ProfileIcons/pwsh.scale-100.png new file mode 100644 index 0000000000000000000000000000000000000000..ac39527332009e8f3fc44177593f441ecbae7fbb GIT binary patch literal 750 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GXl47_SHTgt!8^$DjQNqGM10AAR!w z@T30+AN=2c@BiLA|M%Sfzw74z9oPSFyZV32<^P*5{$F$IN#m^jK*g;K_s-w*0?3H3 znxm@U`s^N!BFdh_n7t4|cH;svGE`9$Tp1jN|5_|$E~RV)LW`(}5{Kd5Mz zDlD(ZCkB)fXXoNKc8lG0;KcR&uY!segl9H&_DyZ?p4i&i+tSuyWMZmn6*O`7vc+4^ zn}jrqtD1H8Pi>pE!^S%#*WBUdy|c#n<PkEgUpX?S3B}*tj_GIHWs5g9^>t=c^<5;sn@H3;h^ebLT zciyq4s)-aec|EOc?Xvret-Pg|H9stO|$m{ z8NL~<3-`SOGSrQo*Pr{pX#Xv*sN!{J{~vwwziR5H^y=Ab&-_2~_I{E+5!~YGlcZ8?6tUU4m!2SP8)zcei9NvHLzimh!P-Mfo z_bOUuK*oeMr@bQ!!6F}fSDflzcA{&^@s34D+ZG&dnRlpp&Vk06`|79fshzsJdeY9y z3EM0BwwCv9nYs13u3L(ZYm&Beyrx5phHZqJb%=_Ezmkc!f}xwNuA{V;wS=0Pn3938 zoK}9rl>h(#S9Q+Tu!~T!2v9Qhmem6)un|)-6qeH#lu`}JX!!K`Yh~MXHS16%Qy&E* zcR4*LX)Rkxb#pOgBN2HWK`AvI*JN$ygrL;wvgSz&MjmqdPBPj+1s3AUMuO66{Nf5) zjI=zg3P>pPipX*ah_Q1E7`w-<-MVku zsuLPM4N}?;!m?TdlFGcIKv{8i9zhm%F8_p*C(m9!ef2T1VXd-#Dlo1;e*83b=6qIm zZYE}ymj1bWPu(?->XSDKP&Kf7^!Ul*t><~f)qzRNFSS+8tyo6aNm9*RL{3{w)k@Yh zT-rE9$}mtu&re*(izIE~KVI(rOorQ&HgNQJaO|3`nv-&ES7(2tb@toc&*w}ip8qb{<%olj@_zLh zM+1aZIt5>wOLG1UniR*Unt3J4*!kAUDf8s|>vHbcrWc&iKBuNq$^KxRmilU%G3aKbEO$%))$t>+*W>Ni6*psY? z)L$a$VMT}}mg}yYX0GmLW^AnwT<_dzX6|!Es6U{A!?|;3=KIck=gbVi|Mr)rQFq?i zu-qq+-ONN2xG4q%o`MAHT>;}{ro?AsG`?(cXdZg;572X8Ji(-Sq$7fcSIziAY*`wyFell?fXug?3x}N{z<(9 zy}8At16dXxjuc&G1M}w0giD1vkjKsupqmNqQ|C&pt0 zk855)?#XgCzBD=vP94n{C5r;6)}C7E{3TAZ%#DClkj>jIsl*;uKZi%vwQSCYbxBZc zI5>t(N}?$VA{HLC%)VZ)vB?BYjZG^sMob!3YDi2d@mYqmQ2F#3o0pcd21-ukxF-|| zpc2V=FE13jAnrAt&j{+QSZ^{m>jZElR}70()H*Y-2#n^>jAUbKGAvI@k*h~tIh07! zUnZdXTg+yyQtzBnmgJjO(EaNr5D_+q5jY2ip~vZ|ieAVElJ$@6Cz!2j0==!P%Ud12 zq;G6YB)qA)1#yd`7E*e({uC%=$#>+!M!2b!9j1F(Dr zAPDlG0#&!mVCR-pHp$e{3)}PWLTO0>{Q6QSNHIEk(E?|L4bt+I38*}+Ek>Pm1tcXb zfvP)YMDN*b!mXM{*mkHK>YkKA>gvVNXKew8y$yc%c0&J8I}H8qgB|G^jF8@4VGHB? z$+A{Af^O?qZ>hHD%?&1!_%KQKpQ(bpf@0`scm&+=0QhQs#=Me)W7e|6N!xt=N~d}U znwzy>Dv#&v-yj_O5mqs~ckh9U>JMNuJ_nccyCMlOpTLEn@far`M7ne28iYQ}O9ptl zqVZw&w&*mO;QWJn^M#^gp>8MA?Fw*-he%4MOZL<#iPR=d4Rl?3+FiIQPGgV>9yd5% zwOh>o?2*9AV<8+m(PPmg=|NG(uu3Zbppd?xS>gK#LBs#I{GZz&&OL$hQMUWt00000 LNkvXXu0mjfqcqb| literal 0 HcmV?d00001 diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/pwsh.scale-200.png b/src/cascadia/CascadiaPackage/ProfileIcons/pwsh.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..72f5e54e31af3712cd9ee4a3b89a6d98e7230c7e GIT binary patch literal 1357 zcmV-T1+w~yP)9 z-+Knq$ls8Rrba>_k*xUBSj)BOCrS|EYgCzuf< zB#ek==ZG0a9npc2T`vi1n#{?1Lz2n!c6GyGh=Kry(UDQD6dWsqW26j@?gmcgWPNYU z{R`yP9wh>n0ee5iEm9Ko@WZKcI8k*FBDESB2@VwT9U&wmgCwL_LS&5JVDOTdx)cdG zh8>Yy@}9DqK^J=;WISwylQr)GMMaCiMQM<9kt#Gd3ehtpOlIqB5CM{Z)_Cqox7IH? zhO6EE{B>hO9DH6=0#sBq4~&+TUX%jaURYQ-on%b+l2-6x79l@kV91aTPM=cOpW6MQ2*>FxoA`7ZNct=Wt7LFcE zhymse=$|wBXNr)7024hiU+BEgs}|>0aJheesQM^0o;%Nz*zt5WytH$dLLFF=cmW3r zP@F!&8yU5R4pM+2V7uN+DONd_49)lVD(hg}HN}&tDBcSZ`piJof(em`1PZ95d%)mh zqR-DzdE_22Fa=0n<>ZCNn4AEu4RxT`#qtmSW}ATUHA#{}gK5DRtO)}rz_`ik;pFED zbpC}2Fc@6pPK`3K|7CCN-3_g0Y9-)bw01*bWefOj3`q(N5|v=gE3gD<8Jl?niWRVU zJRbF=YeFlImV1zj&_Mmsa@fDGfCu)>-C=>tk6!?ny#xMnvRw zz#T!J02BS_@UWK4xsY!JTc5~+&hI{hY|AEzgi*;AVd-%(;6dCxDytoS+6a!m%P?a*3n}YkASW{& ztXUSw&3=^hOvug2gvYE|5U$B=Y=!bYE1e!jJj;%nPpDY~6(eGxyHy&XSaP>5{< z4lHgH)3dlbdo8 +#include +#include +#include -// Legacy GUIDs: -// - PowerShell Core 574e775e-4f2a-5b96-ac1e-a2962a402336 +static constexpr std::wstring_view POWERSHELL_PFN{ L"Microsoft.PowerShell_8wekyb3d8bbwe" }; +static constexpr std::wstring_view POWERSHELL_PREVIEW_PFN{ L"Microsoft.PowerShellPreview_8wekyb3d8bbwe" }; +static constexpr std::wstring_view PWSH_EXE{ L"pwsh.exe" }; +static constexpr std::wstring_view POWERSHELL_ICON{ L"ms-appx:///ProfileIcons/pwsh.png" }; +static constexpr std::wstring_view POWERSHELL_PREVIEW_ICON{ L"ms-appx:///ProfileIcons/pwsh-preview.png" }; -std::wstring_view PowershellCoreProfileGenerator::GetNamespace() +namespace { - return PowershellCoreGeneratorNamespace; -} + enum PowerShellFlags + { + None = 0, -// Method Description: -// - Checks if pwsh is installed, and if it is, creates a profile to launch it. -// Arguments: -// - -// Return Value: -// - a vector with the PowerShell Core profile, if available. -std::vector PowershellCoreProfileGenerator::GenerateProfiles() -{ - std::vector profiles; + // These flags are used as a sort key, so they encode some native ordering. + // They are ordered such that the "most important" flags have the largest + // impact on the sort space. For example, since we want Preview to be very polar + // we give it the highest flag value. + // The "ideal" powershell instance has 0 flags (stable, native, Program Files location) + // + // With this ordering, the sort space ends up being (for PowerShell 6) + // (numerically greater values are on the left; this is flipped in the final sort) + // + // <-- Less Valued .................................... More Valued --> + // | All instances of PS 6 | All PS7 | + // | Preview | Stable | ~~~ | + // | Non-Native | Native | Non-Native | Native | ~~~ | + // | Trd | Pack | Trd | Pack | Trd | Pack | Trd | Pack | ~~~ | + // (where Pack is a stand-in for store, scoop, dotnet, though they have their own orders, + // and Trd is a stand-in for "Traditional" (Program Files)) + // + // In short, flags with larger magnitudes are pushed further down (therefore valued less) + + // distribution method (choose one) + Store = 1 << 0, // distributed via the store + Scoop = 1 << 1, // installed via Scoop + Dotnet = 1 << 2, // installed as a dotnet global tool + Traditional = 1 << 3, // installed in traditional Program Files locations + + // native architecutre (choose one) + WOWARM = 1 << 4, // non-native (Windows-on-Windows, ARM variety) + WOWx86 = 1 << 5, // non-native (Windows-on-Windows, x86 variety) - std::filesystem::path psCoreCmdline; - if (_isPowerShellCoreInstalled(psCoreCmdline)) + // build type (choose one) + Preview = 1 << 6, // preview version + }; + DEFINE_ENUM_FLAG_OPERATORS(PowerShellFlags); + + struct PowerShellInstance { - auto pwshProfile{ CreateDefaultProfile(L"PowerShell Core") }; + int majorVersion; // 0 = we don't know, sort last. + PowerShellFlags flags; + std::filesystem::path executablePath; - pwshProfile.SetCommandline(std::move(psCoreCmdline)); - pwshProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); - pwshProfile.SetColorScheme({ L"Campbell" }); + constexpr bool operator<(const PowerShellInstance& second) const + { + if (majorVersion != second.majorVersion) + { + return majorVersion < second.majorVersion; + } + if (flags != second.flags) + { + return flags > second.flags; // flags are inverted because "0" is ideal; see above + } + return executablePath < second.executablePath; // fall back to path sorting + } - // If powershell core is installed, we'll use that as the default. - // Otherwise, we'll use normal Windows Powershell as the default. - profiles.emplace_back(pwshProfile); - } + // Method Description: + // - Generates a name, based on flags, for a powershell instance. + // Return value: + // - the name + std::wstring Name() const + { + std::wstringstream namestream; + namestream << L"PowerShell"; - return profiles; + if (WI_IsFlagSet(flags, PowerShellFlags::Store)) + { + if (WI_IsFlagSet(flags, PowerShellFlags::Preview)) + { + namestream << L" Preview"; + } + namestream << L" (msix)"; + } + else if (WI_IsFlagSet(flags, PowerShellFlags::Dotnet)) + { + namestream << L" (dotnet global)"; + } + else if (WI_IsFlagSet(flags, PowerShellFlags::Scoop)) + { + namestream << L" (scoop)"; + } + else + { + if (majorVersion < 7) + { + namestream << L" Core"; + } + if (majorVersion != 0) + { + namestream << L" " << majorVersion; + } + if (WI_IsFlagSet(flags, PowerShellFlags::Preview)) + { + namestream << L" Preview"; + } + if (WI_IsFlagSet(flags, PowerShellFlags::WOWx86)) + { + namestream << L" (x86)"; + } + if (WI_IsFlagSet(flags, PowerShellFlags::WOWARM)) + { + namestream << L" (ARM)"; + } + } + return namestream.str(); + } + }; } +using namespace ::TerminalApp; + // Function Description: -// - Returns true if the user has installed PowerShell Core. This will check -// both %ProgramFiles% and %ProgramFiles(x86)%, and will return true if -// powershell core was installed in either location. +// - Finds all powershell instances with the traditional layout under a directory. +// - The "traditional" directory layout requires that pwsh.exe exist in a versioned directory, as in +// ROOT\6\pwsh.exe // Arguments: -// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path. -// Return Value: -// - true iff powershell core (pwsh.exe) is present. -bool PowershellCoreProfileGenerator::_isPowerShellCoreInstalled(std::filesystem::path& cmdline) +// - directory: the directory under which to search +// - flags: flags to apply to all found instances +// - out: the list into which to accumulate these instances. +static void _accumulateTraditionalLayoutPowerShellInstancesInDirectory(std::wstring_view directory, PowerShellFlags flags, std::vector& out) { - return _isPowerShellCoreInstalledInPath(L"%ProgramFiles%", cmdline) || - _isPowerShellCoreInstalledInPath(L"%ProgramFiles(x86)%", cmdline); + const std::filesystem::path root{ wil::ExpandEnvironmentStringsW(directory.data()) }; + if (std::filesystem::exists(root)) + { + for (const auto& versionedDir : std::filesystem::directory_iterator(root)) + { + const auto versionedPath = versionedDir.path(); + const auto executable = versionedPath / PWSH_EXE; + if (std::filesystem::exists(executable)) + { + const auto preview = versionedPath.filename().wstring().find(L"-preview") != std::wstring::npos; + const auto previewFlag = preview ? PowerShellFlags::Preview : PowerShellFlags::None; + out.emplace_back(PowerShellInstance{ std::stoi(versionedPath.filename()), + PowerShellFlags::Traditional | flags | previewFlag, + executable }); + } + } + } } // Function Description: -// - Returns true if the user has installed PowerShell Core. +// - Finds the store package, if one exists, for a given package family name // Arguments: -// - A string that contains an environment-variable string in the form: %variableName%. -// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path. +// - packageFamilyName: the package family name // Return Value: -// - true iff powershell core (pwsh.exe) is present in the given path -bool PowershellCoreProfileGenerator::_isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline) +// - a package, or nullptr. +static winrt::Windows::ApplicationModel::Package _getStorePackage(const std::wstring_view packageFamilyName) noexcept +try +{ + winrt::Windows::Management::Deployment::PackageManager packageManager; + auto foundPackages = packageManager.FindPackagesForUser(L"", packageFamilyName); + auto iterator = foundPackages.First(); + if (!iterator.HasCurrent()) + { + return nullptr; + } + return iterator.Current(); +} +catch (...) +{ + LOG_CAUGHT_EXCEPTION(); + return nullptr; +} + +// Function Description: +// - Finds all powershell instances that have App Execution Aliases in the standard location +// Arguments: +// - out: the list into which to accumulate these instances. +static void _accumulateStorePowerShellInstances(std::vector& out) { - std::wstring programFileEnvNulTerm{ programFileEnv }; - std::filesystem::path psCorePath{ wil::ExpandEnvironmentStringsW(programFileEnvNulTerm.data()) }; - psCorePath /= L"PowerShell"; - if (std::filesystem::exists(psCorePath)) + wil::unique_cotaskmem_string localAppDataFolder; + if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, 0, &localAppDataFolder))) { - for (auto& p : std::filesystem::directory_iterator(psCorePath)) + return; + } + + std::filesystem::path appExecAliasPath{ localAppDataFolder.get() }; + appExecAliasPath /= L"Microsoft"; + appExecAliasPath /= L"WindowsApps"; + + if (std::filesystem::exists(appExecAliasPath)) + { + // App execution aliases for preview powershell + const auto previewPath = appExecAliasPath / POWERSHELL_PREVIEW_PFN; + if (std::filesystem::exists(previewPath)) + { + const auto previewPackage = _getStorePackage(POWERSHELL_PREVIEW_PFN); + if (previewPackage) + { + out.emplace_back(PowerShellInstance{ + gsl::narrow_cast(previewPackage.Id().Version().Major), + PowerShellFlags::Store | PowerShellFlags::Preview, + previewPath / PWSH_EXE }); + } + } + + // App execution aliases for stable powershell + const auto gaPath = appExecAliasPath / POWERSHELL_PFN; + if (std::filesystem::exists(gaPath)) { - psCorePath = p.path(); - psCorePath /= L"pwsh.exe"; - if (std::filesystem::exists(psCorePath)) + const auto gaPackage = _getStorePackage(POWERSHELL_PFN); + if (gaPackage) { - cmdline = psCorePath; - return true; + out.emplace_back(PowerShellInstance{ + gaPackage.Id().Version().Major, + PowerShellFlags::Store, + gaPath / PWSH_EXE, + }); } } } - return false; +} + +// Function Description: +// - Finds a powershell instance that's just a pwsh.exe in a folder. +// - This function cannot determine the version number of such a powershell instance. +// Arguments: +// - directory: the directory under which to search +// - flags: flags to apply to all found instances +// - out: the list into which to accumulate these instances. +static void _accumulatePwshExeInDirectory(const std::wstring_view directory, const PowerShellFlags flags, std::vector& out) +{ + const std::filesystem::path root{ wil::ExpandEnvironmentStringsW(directory.data()) }; + const auto pwshPath = root / PWSH_EXE; + if (std::filesystem::exists(pwshPath)) + { + out.emplace_back(PowerShellInstance{ 0 /* we can't tell */, flags, pwshPath }); + } +} + +// Function Description: +// - Builds a comprehensive priority-ordered list of powershell instances. +// Return value: +// - a comprehensive priority-ordered list of powershell instances. +static std::vector _collectPowerShellInstances() +{ + std::vector versions; + + _accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles%\\PowerShell", PowerShellFlags::None, versions); + +#if defined(_M_AMD64) || defined(_M_ARM64) // No point in looking for WOW if we're not somewhere it exists + _accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(x86)%\\PowerShell", PowerShellFlags::WOWx86, versions); +#endif + +#if defined(_M_ARM64) // no point in looking for WOA if we're not on ARM64 + _accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(Arm)%\\PowerShell", PowerShellFlags::WOWARM, versions); +#endif + + _accumulateStorePowerShellInstances(versions); + + _accumulatePwshExeInDirectory(L"%USERPROFILE%\\.dotnet\\tools", PowerShellFlags::Dotnet, versions); + _accumulatePwshExeInDirectory(L"%USERPROFILE%\\scoop\\shims", PowerShellFlags::Scoop, versions); + + std::sort(versions.rbegin(), versions.rend()); // sort in reverse (best first) + + return versions; +} + +// Legacy GUIDs: +// - PowerShell Core 574e775e-4f2a-5b96-ac1e-a2962a402336 +static constexpr GUID PowershellCoreGuid{ 0x574e775e, 0x4f2a, 0x5b96, { 0xac, 0x1e, 0xa2, 0x96, 0x2a, 0x40, 0x23, 0x36 } }; + +std::wstring_view PowershellCoreProfileGenerator::GetNamespace() +{ + return PowershellCoreGeneratorNamespace; +} + +// Method Description: +// - Checks if pwsh is installed, and if it is, creates a profile to launch it. +// Arguments: +// - +// Return Value: +// - a vector with the PowerShell Core profile, if available. +std::vector PowershellCoreProfileGenerator::GenerateProfiles() +{ + std::vector profiles; + + auto psInstances = _collectPowerShellInstances(); + for (const auto& psI : psInstances) + { + const auto name = psI.Name(); + auto profile{ CreateDefaultProfile(name) }; + profile.SetCommandline(psI.executablePath.wstring()); + profile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY); + profile.SetColorScheme({ L"Campbell" }); + + profile.SetIconPath(WI_IsFlagSet(psI.flags, PowerShellFlags::Preview) ? POWERSHELL_PREVIEW_ICON : POWERSHELL_ICON); + profiles.emplace_back(std::move(profile)); + } + + if (profiles.size() > 0) + { + // Give the first ("algorithmically best") profile the official, and original, "PowerShell Core" GUID. + // This will turn the anchored default profile into "PowerShell Core Latest for Native Architecture through Store" + // (or the closest approximation thereof). It may choose a preview instance as the "best" if it is a higher version. + auto firstProfile = profiles.begin(); + firstProfile->SetGuid(PowershellCoreGuid); + firstProfile->SetName(L"PowerShell"); + } + + return profiles; } diff --git a/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.h b/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.h index 2acae60f8f8..7514fd781aa 100644 --- a/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.h +++ b/src/cascadia/TerminalApp/PowershellCoreProfileGenerator.h @@ -28,9 +28,5 @@ namespace TerminalApp std::wstring_view GetNamespace() override; std::vector GenerateProfiles() override; - - private: - static bool _isPowerShellCoreInstalled(std::filesystem::path& cmdline); - static bool _isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline); }; }; diff --git a/src/cascadia/TerminalApp/Profile.h b/src/cascadia/TerminalApp/Profile.h index 19979eee307..f6adcc1784d 100644 --- a/src/cascadia/TerminalApp/Profile.h +++ b/src/cascadia/TerminalApp/Profile.h @@ -70,6 +70,7 @@ class TerminalApp::Profile final bool HasConnectionType() const noexcept; GUID GetConnectionType() const noexcept; + void SetGuid(GUID guid) noexcept { _guid = guid; } void SetFontFace(std::wstring fontFace) noexcept; void SetColorScheme(std::optional schemeName) noexcept; std::optional& GetSchemeName() noexcept;