1
0
Fork 0
Everything2Everything/src/EverythingToJpeg.Shell/dllmain.cpp
Yun Chan f2c8610ff6 Phase 2: MSIX 패키지 + IExplorerCommand 셸 익스텐션
C++ 셸 익스텐션 (src/EverythingToJpeg.Shell/)
- WRL RuntimeClass 패턴으로 IExplorerCommand 두 핸들러 구현
- Quick(빠른 변환) / Dialog(설정 창) verb를 다른 CLSID로 분리
- Invoke()에서 EverythingToJpeg.exe로 verb + 파일 경로 전달
- VS 2026 빌드 검증, /utf-8 한글 라벨 지원

MSIX 패키징 (packaging/)
- Package.appxmanifest: com:SurrogateServer + desktop4:FileExplorerContextMenus
- 26개 확장자 × 2 verb 자동 노출
- BuildMsix.ps1: dotnet publish + msbuild + makeappx 일관 파이프라인
- CreateDevCert.ps1 / Install-EverythingToJpeg.ps1: 자체 서명 인증서 워크플로
- GenerateAssets.ps1: placeholder 로고 자동 생성

CI/CD
- .github/workflows/release.yml: 태그 푸시 시 미서명 MSIX 자동 빌드 + 첨부

검증
- 50MB MSIX 산출 확인 (packaging/dist/EverythingToJpeg-x64.msix)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:03:16 +09:00

188 lines
5.4 KiB
C++

// EverythingToJpeg shell extension — IExplorerCommand handlers
// Two verbs:
// - QuickCommandHandler → "EverythingToJpeg.exe quick "<paths>""
// - DialogCommandHandler → "EverythingToJpeg.exe dialog "<paths>""
#include "pch.h"
#pragma warning(disable : 4324)
using Microsoft::WRL::ClassicCom;
using Microsoft::WRL::ComPtr;
using Microsoft::WRL::InhibitRoOriginateError;
using Microsoft::WRL::Module;
using Microsoft::WRL::ModuleType;
using Microsoft::WRL::RuntimeClass;
using Microsoft::WRL::RuntimeClassFlags;
namespace {
constexpr const wchar_t* kExeFileName = L"EverythingToJpeg.exe";
std::wstring QuoteForCommandLineArg(const std::wstring& arg) {
const std::wstring quotable_chars(L" \\\"");
if (arg.find_first_of(quotable_chars) == std::wstring::npos) {
return arg;
}
std::wstring out;
out.push_back(L'"');
for (size_t i = 0; i < arg.size(); ++i) {
if (arg[i] == L'\\') {
const size_t start = i;
size_t end = start + 1;
for (; end < arg.size() && arg[end] == L'\\'; ++end) {}
size_t backslash_count = end - start;
if (end == arg.size() || arg[end] == L'"') {
backslash_count *= 2;
}
for (size_t j = 0; j < backslash_count; ++j)
out.push_back(L'\\');
i = end - 1;
}
else if (arg[i] == L'"') {
out.push_back(L'\\');
out.push_back(L'"');
}
else {
out.push_back(arg[i]);
}
}
out.push_back(L'"');
return out;
}
std::filesystem::path ResolveExePath() {
std::filesystem::path module_path{
wil::GetModuleFileNameW<std::wstring>(wil::GetModuleInstanceHandle()) };
module_path = module_path.remove_filename();
module_path /= kExeFileName;
return module_path;
}
HRESULT LaunchAppWithItems(const wchar_t* verb, IShellItemArray* items) {
if (!items) return S_OK;
DWORD count = 0;
RETURN_IF_FAILED(items->GetCount(&count));
if (count == 0) return S_OK;
auto exe_path = ResolveExePath();
auto command = wil::str_printf<std::wstring>(LR"-("%s" %s)-",
exe_path.c_str(), verb);
for (DWORD i = 0; i < count; ++i) {
ComPtr<IShellItem> item;
if (FAILED(items->GetItemAt(i, &item))) continue;
wil::unique_cotaskmem_string path;
if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &path))) continue;
command = wil::str_printf<std::wstring>(LR"-(%s %s)-",
command.c_str(),
QuoteForCommandLineArg(path.get()).c_str());
}
wil::unique_process_information process_info;
STARTUPINFOW startup_info = { sizeof(startup_info) };
RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(
nullptr,
command.data(),
nullptr,
nullptr,
FALSE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
&startup_info,
&process_info));
return S_OK;
}
template <typename Derived>
class CommandHandlerBase : public RuntimeClass<
RuntimeClassFlags<ClassicCom | InhibitRoOriginateError>,
IExplorerCommand>
{
public:
IFACEMETHODIMP GetTitle(IShellItemArray*, PWSTR* name) override {
return SHStrDupW(Derived::Title(), name);
}
IFACEMETHODIMP GetIcon(IShellItemArray*, PWSTR* icon) override {
auto exe = ResolveExePath();
return SHStrDupW(exe.c_str(), icon);
}
IFACEMETHODIMP GetToolTip(IShellItemArray*, PWSTR* infoTip) override {
*infoTip = nullptr;
return E_NOTIMPL;
}
IFACEMETHODIMP GetCanonicalName(GUID* guidCommandName) override {
*guidCommandName = GUID_NULL;
return S_OK;
}
IFACEMETHODIMP GetState(IShellItemArray*, BOOL, EXPCMDSTATE* cmdState) override {
*cmdState = ECS_ENABLED;
return S_OK;
}
IFACEMETHODIMP GetFlags(EXPCMDFLAGS* flags) override {
*flags = ECF_DEFAULT;
return S_OK;
}
IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand** enumCommands) override {
*enumCommands = nullptr;
return E_NOTIMPL;
}
IFACEMETHODIMP Invoke(IShellItemArray* items, IBindCtx*) override {
return LaunchAppWithItems(Derived::Verb(), items);
}
};
} // namespace
class __declspec(uuid("801B2DD3-632C-4731-9510-AEAE09345264"))
QuickCommandHandler final
: public CommandHandlerBase<QuickCommandHandler>
{
public:
static constexpr const wchar_t* Title() { return L"JPEG로 빠른 변환"; }
static constexpr const wchar_t* Verb() { return L"quick"; }
};
class __declspec(uuid("CEBA1DB7-9175-4DF6-A362-490DEA49B598"))
DialogCommandHandler final
: public CommandHandlerBase<DialogCommandHandler>
{
public:
static constexpr const wchar_t* Title() { return L"JPEG로 변환…"; }
static constexpr const wchar_t* Verb() { return L"dialog"; }
};
CoCreatableClass(QuickCommandHandler)
CoCreatableClass(DialogCommandHandler)
CoCreatableClassWrlCreatorMapInclude(QuickCommandHandler)
CoCreatableClassWrlCreatorMapInclude(DialogCommandHandler)
BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) {
return TRUE;
}
_Check_return_
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) {
if (ppv == nullptr) return E_POINTER;
*ppv = nullptr;
return Module<ModuleType::InProc>::GetModule().GetClassObject(rclsid, riid, ppv);
}
__control_entrypoint(DllExport)
STDAPI DllCanUnloadNow(void) {
return Module<ModuleType::InProc>::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE;
}