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

Font Install #5042

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ FOF
foldc
foldcase
FOLDERID
FONTHASH
FORPARSING
foundfr
fsanitize
Expand Down Expand Up @@ -427,6 +428,7 @@ pid
pidl
pidlist
PKCS
PKEY
pkgmgr
pkindex
pkix
Expand All @@ -440,7 +442,10 @@ PRIMARYKEY
processthreads
productcode
PRODUCTICON
propkey
PROPVARIANT
proxystub
pwsz
pscustomobject
pseudocode
PSHOST
Expand Down
4 changes: 3 additions & 1 deletion doc/windows/package-manager/winget/returnCodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ ms.localizationpriority: medium
| 0x8A150084 | -1978335100 | APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED | The Microsoft Store package does not support download command. |
| 0x8A150085 | -1978335099 | APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN | Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have required privilege. |
| 0x8A150086 | -1978335098 | APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE | Downloaded zero byte installer; ensure that your network connection is working properly. |
| 0x8A150087 | -1978335097 | APPINSTALLER_CLI_ERROR_FONT_INSTALL_FAILED | Failed to install font package. |
| 0x8A150088 | -1978335096 | APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_SUPPORTED | Font file is not supported and cannot be installed. |

## Install errors.

Expand All @@ -170,7 +172,7 @@ ms.localizationpriority: medium
| 0x8A150111 | -1978334959 | APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE_BY_APPLICATION | Application is currently in use by another application. |
| 0x8A150112 | -1978334958 | APPINSTALLER_CLI_ERROR_INSTALL_INVALID_PARAMETER | Invalid parameter. |
| 0x8A150113 | -1978334957 | APPINSTALLER_CLI_ERROR_INSTALL_SYSTEM_NOT_SUPPORTED | Package not supported by the system. |
| 0x8A150114 | -1978334956 | APPINSTALLER_CLI_ERROR_INSTALL_UPGRADE_NOT_SUPPORTED | The installer does not support upgrading an existing package. |
| 0x8A150114 | -1978334956 | APPINSTALLER_CLI_ERROR_INSTALL_UPGRADE_NOT_SUPPORTED | The installer does not support upgrading an existing package. |
| 0x8A150115 | -1978334955 | APPINSTALLER_CLI_ERROR_INSTALL_CUSTOM_ERROR | Installation failed with installer custom error. |

## Check for package installed status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"wix",
"burn",
"pwa",
"portable"
"portable",
"font"
],
"description": "Enumeration of supported installer types. InstallerType is required in either root level or individual Installer level"
},
Expand All @@ -80,7 +81,8 @@
"nullsoft",
"wix",
"burn",
"portable"
"portable",
"font"
],
"description": "Enumeration of supported nested installer types contained inside an archive file"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@
"wix",
"burn",
"pwa",
"portable"
"portable",
"font"
],
"description": "Enumeration of supported installer types. InstallerType is required in either root level or individual Installer level"
},
Expand All @@ -182,7 +183,8 @@
"nullsoft",
"wix",
"burn",
"portable"
"portable",
"font"
],
"description": "Enumeration of supported nested installer types contained inside an archive file"
},
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@
<ClInclude Include="ExecutionContext.h" />
<ClInclude Include="ExecutionProgress.h" />
<ClInclude Include="ExecutionReporter.h" />
<ClInclude Include="FontInstaller.h" />
<ClInclude Include="Invocation.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="PortableInstaller.h" />
Expand Down Expand Up @@ -483,6 +484,7 @@
<ClCompile Include="ExecutionContext.cpp" />
<ClCompile Include="ExecutionProgress.cpp" />
<ClCompile Include="ExecutionReporter.cpp" />
<ClCompile Include="FontInstaller.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@
<ClInclude Include="Workflows\FontFlow.h">
<Filter>Workflows</Filter>
</ClInclude>
<ClInclude Include="FontInstaller.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
Expand Down Expand Up @@ -502,6 +505,9 @@
<ClCompile Include="Workflows\FontFlow.cpp">
<Filter>Workflows</Filter>
</ClCompile>
<ClCompile Include="FontInstaller.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
Expand Down
48 changes: 48 additions & 0 deletions src/AppInstallerCLICore/Commands/FontCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "Workflows/CompletionFlow.h"
#include "Workflows/WorkflowBase.h"
#include "Workflows/FontFlow.h"
#include "Workflows/InstallFlow.h"
#include "Resources.h"

namespace AppInstaller::CLI
Expand All @@ -20,6 +21,7 @@ namespace AppInstaller::CLI
{
return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({
std::make_unique<FontListCommand>(FullName()),
std::make_unique<FontInstallCommand>(FullName()),
});
}

Expand All @@ -43,6 +45,52 @@ namespace AppInstaller::CLI
OutputHelp(context.Reporter);
}

std::vector<Argument> FontInstallCommand::GetArguments() const
{
return {
Argument::ForType(Args::Type::Manifest),
};
}

Resource::LocString FontInstallCommand::ShortDescription() const
{
return { Resource::String::FontInstallCommandShortDescription };
}

Resource::LocString FontInstallCommand::LongDescription() const
{
return { Resource::String::FontInstallCommandLongDescription };
}

void FontInstallCommand::Complete(Execution::Context& context, Args::Type valueType) const
{
UNREFERENCED_PARAMETER(valueType);
context.Reporter.Error() << Resource::String::PendingWorkError << std::endl;
ryfu-msft marked this conversation as resolved.
Show resolved Hide resolved
THROW_HR(E_NOTIMPL);
}

Utility::LocIndView FontInstallCommand::HelpLink() const
{
return s_FontCommand_HelpLink;
}

void FontInstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const
{
Argument::ValidateCommonArguments(execArgs);
}

void FontInstallCommand::ExecuteInternal(Execution::Context& context) const
{
if (context.Args.Contains(Execution::Args::Type::Manifest))
{
context <<
Workflow::ReportExecutionStage(ExecutionStage::Discovery) <<
Workflow::GetManifestFromArg <<
Workflow::SelectInstaller <<
Workflow::InstallSinglePackage;
}
}

std::vector<Argument> FontListCommand::GetArguments() const
{
return {
Expand Down
18 changes: 18 additions & 0 deletions src/AppInstallerCLICore/Commands/FontCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ namespace AppInstaller::CLI
void ExecuteInternal(Execution::Context& context) const override;
};

struct FontInstallCommand final : public Command
{
FontInstallCommand(std::string_view parent) : Command("install", parent) {}

std::vector<Argument> GetArguments() const override;

Resource::LocString ShortDescription() const override;
Resource::LocString LongDescription() const override;

void Complete(Execution::Context& context, Execution::Args::Type valueType) const override;

Utility::LocIndView HelpLink() const override;

protected:
void ValidateArgumentsInternal(Execution::Args& execArgs) const override;
void ExecuteInternal(Execution::Context& context) const override;
};

struct FontListCommand final : public Command
{
FontListCommand(std::string_view parent) : Command("list", parent) {}
Expand Down
98 changes: 98 additions & 0 deletions src/AppInstallerCLICore/FontInstaller.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "pch.h"
#include "ExecutionContext.h"
#include "FontInstaller.h"
#include <winget/Fonts.h>
#include <winget/Manifest.h>
#include <winget/ManifestCommon.h>
#include <winget/Filesystem.h>
#include <AppInstallerErrors.h>
#include <AppInstallerRuntime.h>

namespace AppInstaller::CLI::Font
{
namespace
{
constexpr std::wstring_view s_FontsPathSubkey = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts";
constexpr std::wstring_view s_TrueType = L" (TrueType)";

bool IsTrueTypeFont(DWRITE_FONT_FILE_TYPE fileType)
{
return (
fileType == DWRITE_FONT_FILE_TYPE_TRUETYPE ||
fileType == DWRITE_FONT_FILE_TYPE_TRUETYPE_COLLECTION
);
}
}

FontInstaller::FontInstaller(Manifest::ScopeEnum scope) : m_scope(scope)
{
if (scope == Manifest::ScopeEnum::Machine)
{
m_installLocation = Runtime::GetPathTo(Runtime::PathName::FontsMachineInstallLocation);
m_key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_FontsPathSubkey });
}
else
{
m_installLocation = Runtime::GetPathTo(Runtime::PathName::FontsUserInstallLocation);
m_key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_FontsPathSubkey });
}
}

void FontInstaller::Install(const std::vector<FontFile>& fontFiles)
{
for (const auto& fontFile : fontFiles)
{
const auto& filePath = fontFile.FilePath;
const auto& fileName = filePath.filename();
const auto& destinationPath = m_installLocation / fileName;

AICLI_LOG(CLI, Verbose, << "Getting Font title");

std::wstring title = AppInstaller::Fonts::GetFontFileTitle(filePath);

if (IsTrueTypeFont(fontFile.FileType))
{
title += s_TrueType;
JohnMcPMS marked this conversation as resolved.
Show resolved Hide resolved
}

// If font subkey already exists, remove the font file if it exists.
ryfu-msft marked this conversation as resolved.
Show resolved Hide resolved
if (m_key[title].has_value())
{
AICLI_LOG(CLI, Info, << "Existing font subkey found:" << AppInstaller::Utility::ConvertToUTF8(title));
std::filesystem::path existingFontFilePath = { m_key[title]->GetValue<Registry::Value::Type::String>() };

if (m_scope == Manifest::ScopeEnum::Machine)
{
// Font entries in the HKEY_LOCAL_MACHINE hive only have the filename specified as the value. Prepend install location.
existingFontFilePath = m_installLocation / existingFontFilePath;
}

if (std::filesystem::exists(existingFontFilePath))
{
AICLI_LOG(CLI, Info, << "Removing existing font file at:" << existingFontFilePath);
std::filesystem::remove(existingFontFilePath);
Copy link
Member

Choose a reason for hiding this comment

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

If the operation fails, we cannot revert this. Much like my comment on the rest of the operation being reverted on failure, we want some mechanism to put things back the way they were.

I would think that to make it maximally recoverable, one would need to:

  1. Create a temporary portable tracking database for the file and registry changes (probably requires some amount of addition to the tracked things)
  2. Set up a resume that can leverage the database to revert to the previous state

But I don't think we need to be that extreme (handling full on process termination). A more modest approach would have a vector of IOperationSteps that you populate as you go. On exiting the operation, you either invoke Complete or Revert on all of the items, depending on success or failure. In this case, Complete deletes the renamed file, while Revert renames it back to its old state.

}
}

AICLI_LOG(CLI, Info, << "Creating font subkey with name: " << AppInstaller::Utility::ConvertToUTF8(title));
if (m_scope == Manifest::ScopeEnum::Machine)
{
m_key.SetValue(title, fileName, REG_SZ);
Copy link
Member

Choose a reason for hiding this comment

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

Is it truly always a 1:1 registry value to file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I believe so. Whenever I do a manual install of a font file, it creates a new registry value. I haven't seen an example where that hasn't happened yet.

Copy link
Member

Choose a reason for hiding this comment

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

The value names must be unique, so we know that isn't an issue. But that leaves actual question in a more verbose form "Is it possible for multiple values to point to the same file, for instance a .ttc?". Maybe it is acceptable for us to install it as a single entry and the DWrite APIs take care of exposing the contents. But have you installed a TTC via explorer and examined the resulting registry changes?

Copy link
Contributor Author

@ryfu-msft ryfu-msft Dec 19, 2024

Choose a reason for hiding this comment

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

A TTC also only has exactly one registry value when installed. Having multiple registry values point to the same font file doesn't appear to have any effect on how the font appears in the system.

Copy link
Member

Choose a reason for hiding this comment

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

This code has no capacity to clean up on a failure occurring in the middle. The easiest solution might be to reuse the portable database, even if only during the install itself (in memory SQLite). Then an error/exception could trigger a cleanup via the files/registry values written so far.

At a minimum, it would be better to write the registry first so that hopefully the entry shows up in font list and the user can "uninstall" it (which would find no file to remove and just remove the registry).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I implemented a basic uninstall function that will remove all registry entries and font files in the reverse order if installation happens to fail. So cleaning up files, then registry values.

}
else
{
m_key.SetValue(title, destinationPath, REG_SZ);
JohnMcPMS marked this conversation as resolved.
Show resolved Hide resolved
}

AICLI_LOG(CLI, Info, << "Moving font file to: " << destinationPath);
AppInstaller::Filesystem::RenameFile(filePath, destinationPath);
Copy link
Member

Choose a reason for hiding this comment

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

Does this handle the case where the target file already exists? If it deletes that file, this is probably bad. We should choose a new name if it does already exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a check to see if the installation will overwrite an existing font file. If that is true, I show an error message. This is only a temporary solution for now, we need a proper way to bring back those font files that we remove.

}
}

void FontInstaller::Uninstall(const std::wstring& familyName)
{
UNREFERENCED_PARAMETER(familyName);
}
}
33 changes: 33 additions & 0 deletions src/AppInstallerCLICore/FontInstaller.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include <filesystem>
#include <winget/Fonts.h>

namespace AppInstaller::CLI::Font
{
struct FontFile
{
FontFile(std::filesystem::path filePath, DWRITE_FONT_FILE_TYPE fileType)
: FilePath(std::move(filePath)), FileType(fileType) {}

std::filesystem::path FilePath;
DWRITE_FONT_FILE_TYPE FileType;
};

struct FontInstaller
{
FontInstaller(Manifest::ScopeEnum scope);

std::filesystem::path FontFileLocation;

void Install(const std::vector<FontFile>& fontFiles);

void Uninstall(const std::wstring& familyName);

private:
Manifest::ScopeEnum m_scope;
std::filesystem::path m_installLocation;
Registry::Key m_key;
};
}
3 changes: 3 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,10 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(FontFaces);
WINGET_DEFINE_RESOURCE_STRINGID(FontFamily);
WINGET_DEFINE_RESOURCE_STRINGID(FontFamilyNameArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontFileNotSupported);
WINGET_DEFINE_RESOURCE_STRINGID(FontFilePaths);
WINGET_DEFINE_RESOURCE_STRINGID(FontInstallCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontInstallCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontVersion);
Expand Down
Loading
Loading