-
Notifications
You must be signed in to change notification settings - Fork 320
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
MRTCore will also search for [modulename].pri for unpackaged app #814
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. See LICENSE in the project root for license information. | ||
|
||
#include <Windows.h> | ||
|
||
#include <Pathcch.h> | ||
#include "mrm/BaseInternal.h" | ||
#include "mrm/common/platform.h" | ||
#include "mrm/readers/MrmReaders.h" | ||
|
@@ -25,6 +25,7 @@ typedef struct | |
|
||
constexpr wchar_t ResourceUriPrefix[] = L"ms-resource://"; | ||
constexpr int ResourceUriPrefixLength = ARRAYSIZE(ResourceUriPrefix) - 1; | ||
constexpr wchar_t c_defaultPriFilename[] = L"resources.pri"; | ||
|
||
#define INDEX_RESOURCE_ID -1 | ||
#define INDEX_RESOURCE_URI -2 | ||
|
@@ -853,18 +854,21 @@ STDAPI_(void) MrmFreeResource(_In_opt_ void* resource) | |
return; | ||
} | ||
|
||
// Append filename to current module path. If the file doesn't exist, it will | ||
// When filename is provided, append filename to current module path. If the file doesn't exist, it will | ||
// append the filename to parent path. If none exists, file in current module | ||
// path will be returned. | ||
// Visual Studio usually builds the executable to a subfolder with project name | ||
// Visual Studio usually builds the executable to a subfolder with project name for packaged project | ||
// unless TargetPlatformIdentifier is UAP. To accommodate this behavior, we will | ||
// try to search resources.pri from both module path and parent path. | ||
STDAPI MrmGetFilePathFromName(_In_ PCWSTR filename, _Outptr_ PWSTR* filePath) | ||
// | ||
// When filename is not provided or empty, we will search resources.pri under current module | ||
// path. If the file doesn't exist, search [modulename].pri instead. | ||
// For unpackaged app, the built resource name is [modulename].pri. We still search resources.pri | ||
// because that is a supported scenario for inbox MRT (Xaml islands). | ||
STDAPI MrmGetFilePathFromName(_In_opt_ PCWSTR filename, _Outptr_ PWSTR* filePath) | ||
{ | ||
*filePath = nullptr; | ||
|
||
RETURN_HR_IF(E_INVALIDARG, filename == nullptr || *filename == L'\0'); | ||
|
||
wchar_t path[MAX_PATH]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe not as part of this PR, but we should remove usages of MAX_PATH. |
||
DWORD size = ARRAYSIZE(path); | ||
DWORD length = GetModuleFileName(nullptr, path, size); | ||
|
@@ -899,24 +903,38 @@ STDAPI MrmGetFilePathFromName(_In_ PCWSTR filename, _Outptr_ PWSTR* filePath) | |
pointerToPath++; | ||
} | ||
|
||
pointerToPath = pathAllocated ? pathAllocated.get() : path; | ||
rohanp-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
wchar_t moduleFilename[MAX_PATH]; | ||
RETURN_IF_FAILED(StringCchCopyW(moduleFilename, ARRAYSIZE(moduleFilename), lastSlash != nullptr ? lastSlash + 1 : pointerToPath)); | ||
|
||
if (lastSlash != nullptr) | ||
{ | ||
*(lastSlash + 1) = 0; | ||
} | ||
|
||
pointerToPath = pathAllocated ? pathAllocated.get() : path; | ||
|
||
std::unique_ptr<wchar_t, decltype(&MrmFreeResource)> finalPath(nullptr, MrmFreeResource); | ||
|
||
PCWSTR filenameToUse = filename; | ||
if (filename == nullptr || *filename == L'\0') | ||
{ | ||
filenameToUse = c_defaultPriFilename; | ||
} | ||
|
||
// Will build the path at most twice. | ||
// First time using current path. If not exist, do another time with parent path. | ||
// If filename is provided: | ||
// - search under exe path | ||
// - if not exist, search parent path | ||
// If filename is not provided: | ||
// - search under exe path with default name (resources.pri) | ||
// - if not exist, search under same path with [modulename].pri | ||
evelynwu-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (int i = 0; i < 2; i++) | ||
{ | ||
size_t lengthInSizeT; | ||
RETURN_IF_FAILED(StringCchLengthW(pointerToPath, STRSAFE_MAX_CCH, &lengthInSizeT)); | ||
length = static_cast<DWORD>(lengthInSizeT); | ||
|
||
RETURN_IF_FAILED(StringCchLengthW(filename, STRSAFE_MAX_CCH, &lengthInSizeT)); | ||
RETURN_IF_FAILED(StringCchLengthW(filenameToUse, STRSAFE_MAX_CCH, &lengthInSizeT)); | ||
DWORD lengthOfName = static_cast<DWORD>(lengthInSizeT); | ||
|
||
RETURN_IF_FAILED(DWordAdd(length, lengthOfName, &length)); | ||
|
@@ -929,9 +947,11 @@ STDAPI MrmGetFilePathFromName(_In_ PCWSTR filename, _Outptr_ PWSTR* filePath) | |
std::unique_ptr<wchar_t, decltype(&MrmFreeResource)> outputPath(rawOutputPath, MrmFreeResource); | ||
|
||
RETURN_IF_FAILED(StringCchCopyW(outputPath.get(), length, pointerToPath)); | ||
RETURN_IF_FAILED(StringCchCatW(outputPath.get(), length, filename)); | ||
RETURN_IF_FAILED(StringCchCatW(outputPath.get(), length, filenameToUse)); | ||
|
||
if (GetFileAttributes(outputPath.get()) != INVALID_FILE_ATTRIBUTES) | ||
DWORD attributes = GetFileAttributes(outputPath.get()); | ||
|
||
if ((attributes != INVALID_FILE_ATTRIBUTES) && !(attributes & FILE_ATTRIBUTE_DIRECTORY)) | ||
{ | ||
// The file exists. Done. | ||
finalPath.swap(outputPath); | ||
|
@@ -940,16 +960,28 @@ STDAPI MrmGetFilePathFromName(_In_ PCWSTR filename, _Outptr_ PWSTR* filePath) | |
|
||
if (i == 0) | ||
{ | ||
// If none of the file exists, will return the file in current path | ||
// The filename in the first iteration (the file under module path) will be returned if no file is found | ||
// in all iterations. | ||
finalPath.swap(outputPath); | ||
} | ||
|
||
if (secondToLastSlash == nullptr) | ||
{ | ||
break; | ||
} | ||
// Prep for second iteration | ||
if (filename == nullptr || *filename == L'\0') | ||
{ | ||
// Change to [modulename].pri | ||
RETURN_IF_FAILED(PathCchRenameExtension(moduleFilename, ARRAYSIZE(moduleFilename), L"pri")); | ||
filenameToUse = moduleFilename; | ||
} | ||
else | ||
{ | ||
// move to parent folder | ||
if (secondToLastSlash == nullptr) | ||
{ | ||
break; | ||
} | ||
|
||
*(secondToLastSlash + 1) = 0; | ||
*(secondToLastSlash + 1) = 0; | ||
} | ||
} | ||
} | ||
|
||
*filePath = finalPath.release(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. See LICENSE in the project root for license information. | ||
|
||
#include <Windows.h> | ||
|
@@ -560,6 +560,23 @@ TEST_CLASS(BasicTest) | |
} | ||
} | ||
|
||
TEST_METHOD(GetFilePath) | ||
{ | ||
wchar_t* path; | ||
Assert::AreEqual(MrmGetFilePathFromName(L"resources.pri", &path), S_OK); | ||
Assert::IsNotNull(wcsstr(path, L"resources.pri")); | ||
MrmFreeResource(path); | ||
|
||
Assert::AreEqual(MrmGetFilePathFromName(L"something.pri", &path), S_OK); | ||
// Even if the file doesn't exist, we will still return a path. This is for those don't use PRI for resource. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. an existing app may want to migrate to switch to WinUI3, but not ready to switch the resource management yet. They can use ResourceNotFound event to hook up their existing resource management. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding that or part of that as a comment would be good, IMO. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. The comment on 963-964 of MRM.cpp was not enlightening because it didn't explain why that was the desired behavior. |
||
Assert::IsNotNull(wcsstr(path, L"something.pri")); | ||
MrmFreeResource(path); | ||
|
||
Assert::AreEqual(MrmGetFilePathFromName(nullptr, &path), S_OK); | ||
Assert::IsNotNull(wcsstr(path, L"resources.pri")); | ||
MrmFreeResource(path); | ||
} | ||
|
||
private: | ||
void VerifyQualifierValue(UINT32 qualifierCount, PWSTR* qualifierNames, PWSTR* qualifierValues, PCWSTR name, PCWSTR expectedValue) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. See LICENSE in the project root for license information. | ||
|
||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using System.Runtime.InteropServices; | ||
|
||
// General Information about an assembly is controlled through the following | ||
// set of attributes. Change these attribute values to modify the information | ||
// associated with an assembly. | ||
[assembly: AssemblyTitle("UnpackagedTests")] | ||
[assembly: AssemblyDescription("")] | ||
[assembly: AssemblyConfiguration("")] | ||
[assembly: AssemblyTrademark("")] | ||
[assembly: AssemblyCulture("")] | ||
|
||
|
||
// The following GUID is for the ID of the typelib if this project is exposed to COM | ||
[assembly: Guid("b706ab66-8dd1-48eb-a81d-4ee55bc3d6ea")] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. See LICENSE in the project root for license information. | ||
|
||
namespace MrtCoreUnpackagedTests | ||
{ | ||
using System; | ||
using System.ComponentModel; | ||
using System.Data; | ||
using System.IO; | ||
using System.Reflection; | ||
using System.Runtime.InteropServices; | ||
using System.Security; | ||
using WEX.Common.Managed; | ||
using WEX.Logging.Interop; | ||
using WEX.TestExecution; | ||
using WEX.TestExecution.Markup; | ||
using Microsoft.ApplicationModel.Resources; | ||
|
||
#region Helper class for WinRT activation | ||
internal class ActivationContext : IDisposable | ||
{ | ||
private IntPtr m_cookie = (IntPtr)0; | ||
private IntPtr m_ctx = (IntPtr)0; | ||
private bool m_activated = false; | ||
|
||
public ActivationContext() | ||
{ | ||
Activate(); | ||
} | ||
|
||
public void Activate() | ||
{ | ||
if (!m_activated) | ||
{ | ||
string assemblyPath = Assembly.GetExecutingAssembly().Location; | ||
ActivateContext(Path.Combine(Path.GetDirectoryName(assemblyPath), "app.manifest")); | ||
m_activated = true; | ||
} | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
if (m_activated) | ||
{ | ||
NativeMethods.DeactivateActCtx(0, m_cookie); | ||
NativeMethods.ReleaseActCtx(m_ctx); | ||
m_cookie = (IntPtr)0; | ||
m_ctx = (IntPtr)0; | ||
m_activated = false; | ||
} | ||
} | ||
|
||
private void ActivateContext(string manifestPath) | ||
{ | ||
var context = new NativeMethods.ACTCTX(); | ||
context.cbSize = (uint)Marshal.SizeOf(typeof(NativeMethods.ACTCTX)); | ||
context.lpSource = manifestPath; | ||
|
||
m_ctx = NativeMethods.CreateActCtx(ref context); | ||
if (m_ctx == (IntPtr)(-1)) | ||
{ | ||
throw new Win32Exception(Marshal.GetLastWin32Error()); | ||
} | ||
|
||
if (!NativeMethods.ActivateActCtx(m_ctx, out m_cookie)) | ||
{ | ||
throw new Win32Exception(Marshal.GetLastWin32Error()); | ||
} | ||
} | ||
} | ||
|
||
[SuppressUnmanagedCodeSecurity] | ||
internal static class NativeMethods | ||
{ | ||
[DllImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateActCtxW")] | ||
internal static extern IntPtr CreateActCtx(ref ACTCTX pActctx); | ||
|
||
[DllImport("kernel32.dll", SetLastError = true)] | ||
[return: MarshalAs(UnmanagedType.Bool)] | ||
internal static extern bool ActivateActCtx(IntPtr hActCtx, out IntPtr lpCookie); | ||
|
||
[DllImport("kernel32.dll", SetLastError = true)] | ||
[return: MarshalAs(UnmanagedType.Bool)] | ||
internal static extern bool DeactivateActCtx(int dwFlags, IntPtr ulCookie); | ||
|
||
[DllImport("kernel32.dll")] | ||
internal static extern void ReleaseActCtx(IntPtr hActCtx); | ||
|
||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] | ||
internal struct ACTCTX | ||
{ | ||
public UInt32 cbSize; | ||
public UInt32 dwFlags; | ||
public string lpSource; | ||
public UInt16 wProcessorArchitecture; | ||
public UInt16 wLangId; | ||
public string lpAssemblyDirectory; | ||
public string lpResourceName; | ||
public string lpApplicationName; | ||
public IntPtr hModule; | ||
} | ||
} | ||
#endregion | ||
|
||
[TestClass] | ||
public class TestClass | ||
{ | ||
private ActivationContext m_context = new ActivationContext(); | ||
private static string m_assemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); | ||
|
||
[AssemblyInitialize] | ||
public static void ModuleSetup(TestContext testContext) | ||
{ | ||
// Cleanup just in case | ||
File.Delete(Path.Combine(m_assemblyFolder, "resources.pri")); | ||
File.Delete(Path.Combine(m_assemblyFolder, "te.processhost.pri")); | ||
} | ||
|
||
[TestMethod] | ||
public void DefaultResourceManager() | ||
{ | ||
var resourceManager = new ResourceManager(); | ||
var resourceMap = resourceManager.MainResourceMap; | ||
var map = resourceMap.GetSubtree("resources"); | ||
|
||
// No resource file is loaded | ||
Verify.AreEqual(map.ResourceCount, 0u); | ||
var ex = Verify.Throws<Exception>(() => map.GetValue("IDS_MANIFEST_MUSIC_APP_NAME")); | ||
Verify.AreEqual((uint)ex.HResult, 0x80070490); // HRESULT_FROM_WIN32(ERROR_NOT_FOUND) | ||
} | ||
|
||
[TestMethod] | ||
public void DefaultResourceManagerWithResourcePri() | ||
{ | ||
File.Copy(Path.Combine(m_assemblyFolder, "resources.pri.standalone"), Path.Combine(m_assemblyFolder, "resources.pri")); | ||
|
||
var resourceManager = new ResourceManager(); | ||
var resourceMap = resourceManager.MainResourceMap; | ||
var map = resourceMap.GetSubtree("resources"); | ||
Verify.AreNotEqual(map.ResourceCount, 0u); | ||
var resource = map.GetValue("IDS_MANIFEST_MUSIC_APP_NAME").ValueAsString; | ||
Verify.AreEqual(resource, "Groove Music"); | ||
|
||
File.Delete(Path.Combine(m_assemblyFolder, "resources.pri")); | ||
} | ||
|
||
[TestMethod] | ||
public void DefaultResourceManagerWithExePri() | ||
{ | ||
File.Copy(Path.Combine(m_assemblyFolder, "resources.pri.standalone"), Path.Combine(m_assemblyFolder, "te.processhost.pri")); | ||
|
||
var resourceManager = new ResourceManager(); | ||
var resourceMap = resourceManager.MainResourceMap; | ||
var map = resourceMap.GetSubtree("resources"); | ||
Verify.AreNotEqual(map.ResourceCount, 0u); | ||
var resource = map.GetValue("IDS_MANIFEST_MUSIC_APP_NAME").ValueAsString; | ||
Verify.AreEqual(resource, "Groove Music"); | ||
rohanp-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
File.Delete(Path.Combine(m_assemblyFolder, "te.processhost.pri")); | ||
} | ||
|
||
[TestMethod] | ||
public void ResourceManagerWithFile() | ||
{ | ||
var resourceManager = new ResourceManager("resources.pri.standalone"); | ||
var resourceMap = resourceManager.MainResourceMap; | ||
var map = resourceMap.GetSubtree("resources"); | ||
Verify.AreNotEqual(map.ResourceCount, 0u); | ||
var resource = map.GetValue("IDS_MANIFEST_MUSIC_APP_NAME").ValueAsString; | ||
Verify.AreEqual(resource, "Groove Music"); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice!