Introduce Command Not Found module (#26319)

* Introduce Command Not Found module

* rewrite module to depend on WinGet PowerShell module

* address Dongbo's feedback

* try and implement settings UI

* fix SUI build; try and store PowerShell object

* add and use object pool

* apply Dongbo's feedback

* add warm up; implement IPooledObjectPolicy

* Add module interface

* WIP trying to import module from settings

* Add EnableModule.ps1

* spellcheck

* spellcheck again

* Installer. Add DisableModule.ps1

* Fix styling

* Give the user some output from installing

* Prettify the Settings controls

* Add button to check PowerShell 7's version

* Fix Settings Assets paths

* Fix PowerShell 7 output

* Make module enable and disable scripts give better information

* Fix spellcheck

* Fix image files and placeholders

* Don't remove CmdNotFound on upgrade and don't fail on uninstall of
CmdNotFound

* Consistent install module scripts location on debug and installed

* installer: Avoid messageboxes and hide powershell on uninstalling CmdNotFound

* Fix psd1 file resolution when installed

* Fix spellcheck

* Add telemetry events

* Fix gpo files

* If GPO is set, enable/disable module on PT start depending on gpo value

* Cleanup module interface

* Cleanup settings code

* If GPO is set, disable Settings page logic

* Adding icons

* Update settings UI and strings

* Add telemetry for suggestions and feedbacks

* Fix sln file

* Fix build

* minor fixes

* Updating icon

* Remove global.json

* Remove unused PowerShell dependency

* Don't use preview version of Automation and fix NOTICE

* Fix signing

* Fix NOTICE.md

* Fix version checking for getfilesiginforedist.dll

* Fix spellchecker

* Fix README.md

* Fix false positives section in expect.txt

* Add logs to module interface

---------

Co-authored-by: Stefan Markovic <stefan@janeasystems.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Carlos Zamora
2024-01-03 07:43:42 -08:00
committed by GitHub
parent 3b90f73bd0
commit 46f5316858
65 changed files with 1633 additions and 63 deletions

View File

@@ -0,0 +1,67 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Authors>Microsoft Corporation</Authors>
<Product>PowerToys</Product>
<Nullable>enable</Nullable>
<Description>PowerToys CommandNotFound</Description>
<AssemblyName>PowerToys.CmdNotFound</AssemblyName>
<GenerateDependencyFile>false</GenerateDependencyFile>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<SelfContained>true</SelfContained>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>
<!-- SelfContained=true requires RuntimeIdentifier to be set -->
<PropertyGroup Condition="'$(Platform)'=='x64'">
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)'=='ARM64'">
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DefineConstants>TRACE;RELEASE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ObjectPool">
<ExcludeAssets>contentFiles</ExcludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.Management.Automation">
<ExcludeAssets>contentFiles</ExcludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<Content Include="WinGetCommandNotFound.psd1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Management.Automation;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.Feedback;
using System.Management.Automation.Subsystem.Prediction;
namespace WinGetCommandNotFound
{
public sealed class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
internal const string Id = "e5351aa4-dfde-4d4d-bf0f-1a2f5a37d8d6";
public void OnImport()
{
if (!Platform.IsWindows || !IsWinGetInstalled())
{
return;
}
SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, WinGetCommandNotFoundFeedbackPredictor.Singleton);
SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, WinGetCommandNotFoundFeedbackPredictor.Singleton);
}
public void OnRemove(PSModuleInfo psModuleInfo)
{
if (!IsWinGetInstalled())
{
return;
}
SubsystemManager.UnregisterSubsystem<IFeedbackProvider>(new Guid(Id));
SubsystemManager.UnregisterSubsystem<ICommandPredictor>(new Guid(Id));
}
private bool IsWinGetInstalled()
{
// Ensure WinGet is installed
using (var pwsh = PowerShell.Create(RunspaceMode.CurrentRunspace))
{
var results = pwsh.AddCommand("Get-Command")
.AddParameter("Name", "winget")
.AddParameter("CommandType", "Application")
.Invoke();
if (results.Count is 0)
{
return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using Microsoft.Extensions.ObjectPool;
namespace WinGetCommandNotFound
{
public sealed class PooledPowerShellObjectPolicy : IPooledObjectPolicy<PowerShell>
{
private static readonly string[] WingetClientModuleName = new[] { "Microsoft.WinGet.Client" };
public PowerShell Create()
{
var iss = InitialSessionState.CreateDefault2();
iss.ImportPSModule(WingetClientModuleName);
return PowerShell.Create(iss);
}
public bool Return(PowerShell obj)
{
if (obj != null)
{
obj.Commands.Clear();
obj.Streams.ClearStreams();
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace WinGetCommandNotFound.Telemetry
{
[EventData]
public class CmdNotFoundFeedbackProvidedEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace WinGetCommandNotFound.Telemetry
{
[EventData]
public class CmdNotFoundSuggestionProvidedEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -0,0 +1,11 @@
@{
ModuleVersion = '0.1.0'
GUID = '28c9afa2-92e5-413e-8e53-44b2d7a83ac6'
Author = 'Carlos Zamora'
CompanyName = "Microsoft Corporation"
Copyright = "Copyright (c) Microsoft Corporation."
Description = 'Enable suggestions on how to install missing commands via winget'
PowerShellVersion = '7.4'
NestedModules = @('PowerToys.CmdNotFound.dll')
RequiredModules = @(@{ModuleName = 'Microsoft.WinGet.Client'; ModuleVersion = "0.2.1"; })
}

View File

@@ -0,0 +1,206 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Management.Automation;
using System.Management.Automation.Subsystem.Feedback;
using System.Management.Automation.Subsystem.Prediction;
using Microsoft.Extensions.ObjectPool;
using Microsoft.PowerToys.Telemetry;
namespace WinGetCommandNotFound
{
public sealed class WinGetCommandNotFoundFeedbackPredictor : IFeedbackProvider, ICommandPredictor
{
private readonly Guid _guid;
private readonly ObjectPool<PowerShell> _pool;
private const int _maxSuggestions = 20;
private List<string>? _candidates;
private bool _warmedUp;
public static WinGetCommandNotFoundFeedbackPredictor Singleton { get; } = new WinGetCommandNotFoundFeedbackPredictor(Init.Id);
private WinGetCommandNotFoundFeedbackPredictor(string guid)
{
_guid = new Guid(guid);
var provider = new DefaultObjectPoolProvider();
_pool = provider.Create(new PooledPowerShellObjectPolicy());
_pool.Return(_pool.Get());
Task.Run(() => WarmUp());
}
public Guid Id => _guid;
public string Name => "Windows Package Manager - WinGet";
public string Description => "Finds missing commands that can be installed via WinGet.";
public Dictionary<string, string>? FunctionsToDefine => null;
private void WarmUp()
{
var ps = _pool.Get();
try
{
ps.AddCommand("Find-WinGetPackage")
.AddParameter("Count", 1)
.Invoke();
}
finally
{
_pool.Return(ps);
_warmedUp = true;
}
}
/// <summary>
/// Gets feedback based on the given commandline and error record.
/// </summary>
public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token)
{
var target = (string)context.LastError!.TargetObject;
if (target is not null)
{
bool tooManySuggestions = false;
string packageMatchFilterField = "command";
var pkgList = FindPackages(target, ref tooManySuggestions, ref packageMatchFilterField);
if (pkgList.Count == 0)
{
return null;
}
// Build list of suggestions
_candidates = new List<string>();
foreach (var pkg in pkgList)
{
_candidates.Add(string.Format(CultureInfo.InvariantCulture, "winget install --id {0}", pkg.Members["Id"].Value.ToString()));
}
// Build footer message
var footerMessage = tooManySuggestions ?
string.Format(CultureInfo.InvariantCulture, "Additional results can be found using \"winget search --{0} {1}\"", packageMatchFilterField, target) :
null;
PowerToysTelemetry.Log.WriteEvent(new Telemetry.CmdNotFoundFeedbackProvidedEvent());
return new FeedbackItem(
"Try installing this package using winget:",
_candidates,
footerMessage,
FeedbackDisplayLayout.Portrait);
}
return null;
}
private Collection<PSObject> FindPackages(string query, ref bool tooManySuggestions, ref string packageMatchFilterField)
{
if (!_warmedUp)
{
return new Collection<PSObject>();
}
var ps = _pool.Get();
try
{
var common = new Hashtable()
{
["Source"] = "winget",
};
// 1) Search by command
var pkgList = ps.AddCommand("Find-WinGetPackage")
.AddParameter("Command", query)
.AddParameter("MatchOption", "StartsWithCaseInsensitive")
.AddParameters(common)
.Invoke();
if (pkgList.Count > 0)
{
tooManySuggestions = pkgList.Count > _maxSuggestions;
packageMatchFilterField = "command";
return pkgList;
}
// 2) No matches found,
// search by name
ps.Commands.Clear();
pkgList = ps.AddCommand("Find-WinGetPackage")
.AddParameter("Name", query)
.AddParameter("MatchOption", "ContainsCaseInsensitive")
.AddParameters(common)
.Invoke();
if (pkgList.Count > 0)
{
tooManySuggestions = pkgList.Count > _maxSuggestions;
packageMatchFilterField = "name";
return pkgList;
}
// 3) No matches found,
// search by moniker
ps.Commands.Clear();
pkgList = ps.AddCommand("Find-WinGetPackage")
.AddParameter("Moniker", query)
.AddParameter("MatchOption", "ContainsCaseInsensitive")
.AddParameters(common)
.Invoke();
tooManySuggestions = pkgList.Count > _maxSuggestions;
packageMatchFilterField = "moniker";
return pkgList;
}
finally
{
_pool.Return(ps);
}
}
public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
{
return feedback switch
{
PredictorFeedbackKind.CommandLineAccepted => true,
_ => false,
};
}
public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken)
{
if (_candidates is not null)
{
string input = context.InputAst.Extent.Text;
List<PredictiveSuggestion>? result = null;
foreach (string c in _candidates)
{
if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase))
{
result ??= new List<PredictiveSuggestion>(_candidates.Count);
result.Add(new PredictiveSuggestion(c));
}
}
if (result is not null)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.CmdNotFoundSuggestionProvidedEvent());
return new SuggestionPackage(result);
}
}
return default;
}
public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
{
// Reset the candidate state.
_candidates = null;
}
}
}

View File

@@ -0,0 +1,108 @@
// Microsoft Visual C++ generated resource script.
//
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// String Table
//
STRINGTABLE
BEGIN
IDS_CMD_NOT_FOUND_NAME "Command Not Found"
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{0014d652-901f-4456-8d65-06fc5f997fb0}</ProjectGuid>
<RootNamespace>CmdNotFoundModuleInterface</RootNamespace>
<TargetName>PowerToys.CmdNotFoundModuleInterface</TargetName>
<PlatformToolset>v143</PlatformToolset>
<ProjectName>CmdNotFoundModuleInterface</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;CMDNOTFOUNDMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;CMDNOTFOUNDMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CmdNotFoundModuleInterface.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CmdNotFoundModuleInterface.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,161 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <filesystem>
#include <string>
#include <common/logger/logger.h>
#include <common/logger/logger_settings.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/gpo.h>
#include <common/utils/logger_helper.h>
#include <common/utils/process_path.h>
#include <common/utils/resources.h>
#include <interface/powertoy_module_interface.h>
#include "resource.h"
#include "trace.h"
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
const static wchar_t* MODULE_NAME = L"Command Not Found";
const static wchar_t* MODULE_DESC = L"A module that detects an error thrown by a command in PowerShell and suggests a relevant WinGet package to install, if available.";
inline const std::wstring ModuleKey = L"CmdNotFound";
class CmdNotFound : public PowertoyModuleIface
{
std::wstring app_name;
std::wstring app_key;
private:
bool m_enabled = false;
void install_module()
{
auto module_path = get_module_folderpath();
std::string command = "pwsh.exe";
command += " ";
command += "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + winrt::to_string(module_path) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\EnableModule.ps1" + "\"" + " -scriptPath \"" + winrt::to_string(module_path) + "\"";
int ret = system(command.c_str());
if (ret != 0)
{
Logger::error("Running EnableModule.ps1 script failed.");
}
else
{
Logger::info("Module installed successfully.");
Trace::EnableCmdNotFoundGpo(true);
}
}
void uninstall_module()
{
auto module_path = get_module_folderpath();
std::string command = "pwsh.exe";
command += " ";
command += "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + winrt::to_string(module_path) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\DisableModule.ps1" + "\"";
int ret = system(command.c_str());
if (ret != 0)
{
Logger::error("Running EnableModule.ps1 script failed.");
}
else
{
Logger::info("Module uninstalled successfully.");
Trace::EnableCmdNotFoundGpo(false);
}
}
public:
CmdNotFound()
{
app_name = GET_RESOURCE_STRING(IDS_CMD_NOT_FOUND_NAME);
app_key = ModuleKey;
std::filesystem::path logFilePath(PTSettingsHelper::get_module_save_folder_location(this->app_key));
logFilePath.append(LogSettings::cmdNotFoundLogPath);
Logger::init(LogSettings::cmdNotFoundLoggerName, logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location());
Logger::info("CmdNotFound object is constructing");
powertoys_gpo::gpo_rule_configured_t gpo_rule_configured_value = gpo_policy_enabled_configuration();
if (gpo_rule_configured_value == powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_enabled)
{
install_module();
m_enabled = true;
}
else if (gpo_rule_configured_value == powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_disabled)
{
uninstall_module();
m_enabled = false;
}
}
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::getConfiguredCmdNotFoundEnabledValue();
}
virtual void destroy() override
{
delete this;
}
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
virtual const wchar_t* get_key() override
{
return app_key.c_str();
}
virtual bool get_config(wchar_t* /*buffer*/, int* /*buffer_size*/) override
{
return false;
}
virtual void set_config(const wchar_t* config) override
{
}
virtual void enable()
{
}
virtual void disable()
{
}
virtual bool is_enabled() override
{
return m_enabled;
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new CmdNotFound();
}

View File

@@ -0,0 +1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header
#include "pch.h"
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.

View File

@@ -0,0 +1,16 @@
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.
#ifndef PCH_H
#define PCH_H
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>
#include <ProjectTelemetry.h>
#endif //PCH_H

View File

@@ -0,0 +1,21 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Awake.rc
//
#define IDS_CMD_NOT_FOUND_NAME 101
#define FILE_DESCRIPTION "PowerToys Command Not Found"
#define INTERNAL_NAME "PowerToys.CmdNotFoundModuleInterface"
#define ORIGINAL_FILENAME "PowerToys.CmdNotFoundModuleInterface.dll"
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View File

@@ -0,0 +1,30 @@
#include "pch.h"
#include "trace.h"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider()
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider()
{
TraceLoggingUnregister(g_hProvider);
}
// Log if the user has CmdNotFound enabled or disabled
void Trace::EnableCmdNotFoundGpo(const bool enabled) noexcept
{
TraceLoggingWrite(
g_hProvider,
"CmdNotFound_EnableCmdNotFound",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}

View File

@@ -0,0 +1,11 @@
#pragma once
class Trace
{
public:
static void RegisterProvider();
static void UnregisterProvider();
// Log if the user has CmdNotFound enabled or disabled
static void EnableCmdNotFoundGpo(const bool enabled) noexcept;
};