diff --git a/_msbuild.py b/_msbuild.py index 270e099..c651e72 100644 --- a/_msbuild.py +++ b/_msbuild.py @@ -202,6 +202,28 @@ def mainw_exe(name): mainw_exe("pythonw"), main_exe("python3"), mainw_exe("pythonw3"), + + CProject("pyshellext", + VersionInfo( + FileDescription="Python shell extension", + OriginalFilename="pyshellext.dll", + ), + Property('DynamicLibcppLinkage', 'true'), + ItemDefinition('ClCompile', + LanguageStandard='stdcpp20', + RuntimeLibrary='MultiThreaded', + ), + ItemDefinition('Link', + AdditionalDependencies=Prepend("RuntimeObject.lib;"), + SubSystem='WINDOWS', + ModuleDefinitionFile='$(SourceRootDir)src\\pyshellext\\pyshellext.def', + ), + Manifest('default.manifest'), + CSourceFile('shellext.cpp'), + ResourceFile('pyshellext.rc'), + SourceFile('pyshellext.def'), + source='src/pyshellext', + ) ) @@ -300,6 +322,7 @@ def init_METADATA(): fileversion = _make_xyzw_version(METADATA["Version"], ",") for vi in PACKAGE.findall("**/VersionInfo"): vi.from_metadata(METADATA) + vi.options["LegalCopyright"] = "Copyright (c) Python Software Foundation. All Rights Reserved." vi.options["FILEVERSION"] = fileversion diff --git a/src/pymanager/appxmanifest.xml b/src/pymanager/appxmanifest.xml index f97b236..bb872fb 100644 --- a/src/pymanager/appxmanifest.xml +++ b/src/pymanager/appxmanifest.xml @@ -2,6 +2,7 @@ + + + + + + + + + + + + + + diff --git a/src/pymanager/default.manifest b/src/pymanager/default.manifest index 5c7d957..c1a14ad 100644 --- a/src/pymanager/default.manifest +++ b/src/pymanager/default.manifest @@ -9,10 +9,6 @@ - - - - diff --git a/src/pymanager/msi.wxs b/src/pymanager/msi.wxs index ffa4824..e843fe2 100644 --- a/src/pymanager/msi.wxs +++ b/src/pymanager/msi.wxs @@ -44,6 +44,11 @@ + + + + @@ -56,6 +61,11 @@ + + diff --git a/src/pyshellext/default.manifest b/src/pyshellext/default.manifest new file mode 100644 index 0000000..c1a14ad --- /dev/null +++ b/src/pyshellext/default.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + true + + + diff --git a/src/pyshellext/idle.ico b/src/pyshellext/idle.ico new file mode 100644 index 0000000..2aa9a83 Binary files /dev/null and b/src/pyshellext/idle.ico differ diff --git a/src/pyshellext/py.ico b/src/pyshellext/py.ico new file mode 100644 index 0000000..1d8a79b Binary files /dev/null and b/src/pyshellext/py.ico differ diff --git a/src/pyshellext/pyshellext.def b/src/pyshellext/pyshellext.def new file mode 100644 index 0000000..751bc6c --- /dev/null +++ b/src/pyshellext/pyshellext.def @@ -0,0 +1,3 @@ +EXPORTS + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE diff --git a/src/pyshellext/pyshellext.rc b/src/pyshellext/pyshellext.rc new file mode 100644 index 0000000..d5edbb2 --- /dev/null +++ b/src/pyshellext/pyshellext.rc @@ -0,0 +1,4 @@ +1 ICON DISCARDABLE "python.ico" +2 ICON DISCARDABLE "pythonw.ico" +3 ICON DISCARDABLE "py.ico" +4 ICON DISCARDABLE "idle.ico" diff --git a/src/pyshellext/python.ico b/src/pyshellext/python.ico new file mode 100644 index 0000000..b8a38ef Binary files /dev/null and b/src/pyshellext/python.ico differ diff --git a/src/pyshellext/pythonw.ico b/src/pyshellext/pythonw.ico new file mode 100644 index 0000000..6195d43 Binary files /dev/null and b/src/pyshellext/pythonw.ico differ diff --git a/src/pyshellext/shellext.cpp b/src/pyshellext/shellext.cpp new file mode 100644 index 0000000..35c0368 --- /dev/null +++ b/src/pyshellext/shellext.cpp @@ -0,0 +1,420 @@ +// Support back to Windows 10 +#define _WIN32_WINNT _WIN32_WINNT_WIN10 +#include + +// Use WRL to define a classic COM class +#define __WRL_CLASSIC_COM__ +#include + +using namespace Microsoft::WRL; + +#include +#include +#include +#include + +#include +#include + +static HINSTANCE hModule; + +#define CLSID_IDLE_COMMAND "{C7E29CB0-9691-4DE8-B72B-6719DDC0B4A1}" +#define CLSID_LAUNCH_COMMAND "{F7209EE3-FC96-40F4-8C3F-4B7D3994370D}" +#define CLSID_COMMAND_ENUMERATOR "{F82C8CD5-A69C-45CC-ADC6-87FC5F4A7429}" + + +struct IdleData { + std::wstring title; + std::wstring exe; + std::wstring idle; +}; + + +static LRESULT RegReadStr(HKEY key, LPCWSTR valueName, std::wstring& result) +{ + DWORD reg_type; + while (true) { + DWORD cch = result.size() * sizeof(result[0]); + LRESULT err = RegQueryValueEx(key, valueName, NULL, ®_type, + (LPBYTE)result.data(), &cch); + cch /= sizeof(result[0]); + if (err == ERROR_SUCCESS && reg_type == REG_SZ) { + result.resize(cch); + while (!result.empty() && result.back() == L'\0') { + result.pop_back(); + } + return err; + } + if (err && err != ERROR_MORE_DATA) { + return err; + } + if (reg_type != REG_SZ) { + return ERROR_INVALID_DATA; + } + if (cch <= result.size()) { + return err; + } + result.resize(cch); + } +} + + +static HRESULT ReadIdleInstalls(std::vector &idles, HKEY hive, REGSAM flags) +{ + HKEY hkPythonCore = NULL, hkTag = NULL, hkInstall = NULL; + LSTATUS err = RegOpenKeyExW( + hive, + L"Software\\Python\\PythonCore", + 0, + KEY_READ | flags, + &hkPythonCore + ); + + for (DWORD i = 0; !err && i < 64; ++i) { + wchar_t name[128]; + DWORD cchName = sizeof(name) / sizeof(name[0]); + err = RegEnumKeyExW(hkPythonCore, i, name, &cchName, NULL, NULL, NULL, NULL); + if (!err) { + err = RegOpenKeyExW(hkPythonCore, name, 0, KEY_READ, &hkTag); + } + if (!err) { + err = RegOpenKeyExW(hkTag, L"InstallPath", 0, KEY_READ, &hkInstall); + } + if (err) { + break; + } + + IdleData data; + + err = RegReadStr(hkTag, L"DisplayName", data.title); + if (err) { + data.title = std::wstring(L"Python ") + name; + } + + err = RegReadStr(hkInstall, L"WindowedExecutablePath", data.exe); + if (err == ERROR_FILE_NOT_FOUND || err == ERROR_INVALID_DATA) { + err = RegReadStr(hkInstall, L"ExecutablePath", data.exe); + if (err == ERROR_FILE_NOT_FOUND || err == ERROR_INVALID_DATA) { + err = RegReadStr(hkInstall, NULL, data.exe); + if (!err) { + if (data.exe.back() != L'\\') { + data.exe += L"\\python.exe"; + } else { + data.exe += L"python.exe"; + } + } + } + } + if (err) { + break; + } + + err = RegReadStr(hkInstall, L"IdlePath", data.idle); + if (err == ERROR_FILE_NOT_FOUND || err == ERROR_INVALID_DATA) { + err = RegReadStr(hkInstall, NULL, data.idle); + if (!err) { + if (data.idle.back() != L'\\') { + data.idle += L"\\Lib\\idlelib\\idle.pyw"; + } else { + data.idle += L"Lib\\idlelib\\idle.pyw"; + } + } + } + if (err) { + break; + } + + RegCloseKey(hkInstall); + hkInstall = NULL; + RegCloseKey(hkTag); + hkTag = NULL; + + if (GetFileAttributesW(data.exe.c_str()) != INVALID_FILE_ATTRIBUTES + && GetFileAttributesW(data.idle.c_str()) != INVALID_FILE_ATTRIBUTES) { + idles.push_back(data); + } + } + if (hkInstall) { + RegCloseKey(hkInstall); + } + if (hkTag) { + RegCloseKey(hkTag); + } + if (hkPythonCore) { + RegCloseKey(hkPythonCore); + } + if (err && err != ERROR_NO_MORE_ITEMS && err != ERROR_FILE_NOT_FOUND) { + return HRESULT_FROM_WIN32(err); + } + return S_OK; +} + +class DECLSPEC_UUID(CLSID_LAUNCH_COMMAND) LaunchCommand + : public RuntimeClass, IExplorerCommand> +{ + std::wstring title; + std::wstring exe; + std::wstring idle; +public: + LaunchCommand(const IdleData &data) : title(data.title), exe(data.exe), idle(data.idle) + { } + + // IExplorerCommand + IFACEMETHODIMP GetTitle(IShellItemArray *psiItemArray, LPWSTR *ppszName) + { + *ppszName = (LPWSTR)CoTaskMemAlloc((title.size() + 1) * sizeof(WCHAR)); + wcscpy_s(*ppszName, title.size() + 1, title.data()); + return S_OK; + } + + IFACEMETHODIMP GetIcon(IShellItemArray *psiItemArray, LPWSTR *ppszIcon) + { + *ppszIcon = NULL; + return E_NOTIMPL; + } + + IFACEMETHODIMP GetToolTip(IShellItemArray *psiItemArray, LPWSTR *ppszInfotip) + { + *ppszInfotip = NULL; + return E_NOTIMPL; + } + + IFACEMETHODIMP GetCanonicalName(GUID* pguidCommandName) + { + *pguidCommandName = __uuidof(LaunchCommand); + return S_OK; + } + + IFACEMETHODIMP GetState(IShellItemArray *psiItemArray, BOOL fOkToBeSlow, EXPCMDSTATE *pCmdState) + { + *pCmdState = ECS_ENABLED; + return S_OK; + } + + IFACEMETHODIMP Invoke(IShellItemArray *psiItemArray, IBindCtx *pbc) + { + std::wstring parameters; + if (idle.find(L' ') != idle.npos) { + parameters = L"\"" + idle + L"\""; + } else { + parameters = idle; + } + + HRESULT hr; + DWORD count; + psiItemArray->GetCount(&count); + for (DWORD i = 0; i < count; ++i) { + PWSTR path; + IShellItem *psi; + hr = psiItemArray->GetItemAt(0, &psi); + if (FAILED(hr)) + continue; + hr = psi->GetDisplayName(SIGDN_FILESYSPATH, &path); + psi->Release(); + if (FAILED(hr)) + continue; + + if (wcschr(path, L' ')) { + parameters += L" \""; + parameters += path; + parameters += L"\""; + } else { + parameters += L" "; + parameters += path; + } + CoTaskMemFree(path); + } + + SHELLEXECUTEINFOW sei = { + sizeof(SHELLEXECUTEINFOW), + SEE_MASK_NO_CONSOLE | SEE_MASK_NOASYNC, + NULL, + NULL, + exe.c_str(), + parameters.c_str(), + NULL + }; + OutputDebugStringW(L"IdleCommand::Invoke"); + OutputDebugStringW(exe.c_str()); + OutputDebugStringW(parameters.c_str()); + ShellExecuteExW(&sei); + return S_OK; + } + + IFACEMETHODIMP GetFlags(EXPCMDFLAGS *pFlags) + { + *pFlags = ECF_DEFAULT; + return S_OK; + } + + IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand **ppEnum) + { + *ppEnum = NULL; + return E_NOTIMPL; + } +}; + + +class DECLSPEC_UUID(CLSID_COMMAND_ENUMERATOR) CommandEnumerator + : public RuntimeClass, IEnumExplorerCommand> +{ + std::vector idles; + size_t index; +public: + CommandEnumerator(std::vector idles, size_t index) + : idles(idles), index(index) { } + + IFACEMETHODIMP Clone(IEnumExplorerCommand **ppenum) + { + return Make(idles, index) + ->QueryInterface(IID_IEnumExplorerCommand, (void **)ppenum); + } + + IFACEMETHODIMP Next(ULONG celt, IExplorerCommand **pUICommand, ULONG *pceltFetched) + { + ULONG c = 0; + while (celt-- && index < idles.size()) { + *pUICommand = Make(idles[index]).Detach(); + index += 1; + c += 1; + } + if (pceltFetched) { + *pceltFetched = c; + } + return c ? S_OK : S_FALSE; + } + + IFACEMETHODIMP Reset() + { + index = 0; + return S_OK; + } + + IFACEMETHODIMP Skip(ULONG celt) + { + index += celt; + return S_OK; + } +}; + + +class DECLSPEC_UUID(CLSID_IDLE_COMMAND) IdleCommand + : public RuntimeClass, IExplorerCommand> +{ + std::vector idles; + std::wstring iconPath; + std::wstring title; +public: + IdleCommand() : title(L"Edit in &IDLE") + { + HRESULT hr; + + DWORD cch = 260; + while (iconPath.size() < cch) { + iconPath.resize(cch); + cch = GetModuleFileNameW(hModule, iconPath.data(), iconPath.size()); + } + iconPath.resize(cch); + if (cch) { + iconPath += L",-4"; + } + + hr = ReadIdleInstalls(idles, HKEY_LOCAL_MACHINE, KEY_WOW64_32KEY); + if (SUCCEEDED(hr)) { + hr = ReadIdleInstalls(idles, HKEY_LOCAL_MACHINE, KEY_WOW64_64KEY); + } + if (SUCCEEDED(hr)) { + hr = ReadIdleInstalls(idles, HKEY_CURRENT_USER, 0); + } + + if (FAILED(hr)) { + wchar_t buffer[512]; + swprintf_s(buffer, L"IdleCommand error 0x%08X", (DWORD)hr); + OutputDebugStringW(buffer); + idles.clear(); + } + } + + // IExplorerCommand + IFACEMETHODIMP GetTitle(IShellItemArray *psiItemArray, LPWSTR *ppszName) + { + *ppszName = (LPWSTR)CoTaskMemAlloc((title.size() + 1) * sizeof(WCHAR)); + wcscpy_s(*ppszName, title.size() + 1, title.c_str()); + return S_OK; + } + + IFACEMETHODIMP GetIcon(IShellItemArray *psiItemArray, LPWSTR *ppszIcon) + { + if (!iconPath.empty()) { + *ppszIcon = (LPWSTR)CoTaskMemAlloc((iconPath.size() + 1) * sizeof(WCHAR)); + wcscpy_s(*ppszIcon, iconPath.size() + 1, iconPath.c_str()); + return S_OK; + } else { + *ppszIcon = NULL; + return E_NOTIMPL; + } + } + + IFACEMETHODIMP GetToolTip(IShellItemArray *psiItemArray, LPWSTR *ppszInfotip) + { + *ppszInfotip = NULL; + return E_NOTIMPL; + } + + IFACEMETHODIMP GetCanonicalName(GUID* pguidCommandName) + { + *pguidCommandName = __uuidof(IdleCommand); + return S_OK; + } + + IFACEMETHODIMP GetState(IShellItemArray *psiItemArray, BOOL fOkToBeSlow, EXPCMDSTATE *pCmdState) + { + *pCmdState = idles.size() ? ECS_ENABLED : ECS_HIDDEN; + return S_OK; + } + + IFACEMETHODIMP Invoke(IShellItemArray *psiItemArray, IBindCtx *pbc) + { + return E_NOTIMPL; + } + + IFACEMETHODIMP GetFlags(EXPCMDFLAGS *pFlags) + { + *pFlags = ECF_HASSUBCOMMANDS; + return S_OK; + } + + IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand **ppEnum) + { + *ppEnum = Make( + std::vector{std::rbegin(idles), std::rend(idles)}, + 0 + ).Detach(); + return S_OK; + } +}; + + +CoCreatableClass(IdleCommand); + + +STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, _COM_Outptr_ void** ppv) +{ + return Module::GetModule().GetClassObject(rclsid, riid, ppv); +} + + +STDAPI DllCanUnloadNow() +{ + return Module::GetModule().Terminate() ? S_OK : S_FALSE; +} + + +STDAPI_(BOOL) DllMain(_In_opt_ HINSTANCE hinst, DWORD reason, _In_opt_ void*) +{ + if (reason == DLL_PROCESS_ATTACH) { + hModule = hinst; + DisableThreadLibraryCalls(hinst); + } + return TRUE; +}