Skip to content

Commit c9813b7

Browse files
jaminmcAnulap
andcommitted
Integrate Windows MSCMS wrapper with safe dependency and API fixes
Port the PR eoyilmaz#591 MSCMS manager/wrapper and tests into the eoyilmaz#552 branch while preserving current cross-platform behavior and dependency set. Fix retry semantics, WCS API signature/size handling, and English test docstrings for safer Windows profile management integration. Co-authored-by: Anulap <perpetuallybored@yandex.ru>
1 parent 1b3164e commit c9813b7

11 files changed

Lines changed: 1679 additions & 221 deletions

DisplayCAL/icc_profile.py

Lines changed: 12 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,13 @@ def which(self, executable: str, paths: None | list[str] = None) -> None | str:
107107
from DisplayCAL.util_os import dlopen, which
108108
elif sys.platform == "win32":
109109
from DisplayCAL import util_win
110+
from DisplayCAL.mscms import WCSManagerProxy
110111

111112
if sys.getwindowsversion() < (6,):
112113
# WCS only available under Vista and later
113114
mscms = None
114115
else:
115-
from DisplayCAL.win_handles import (
116-
get_handle_name,
117-
get_handle_type,
118-
get_process_handles,
119-
)
120-
121-
mscms = util_win._get_mscms_windll()
122-
123-
win_ver = util_win.win_ver()
124-
win10_1903 = (
125-
win_ver[0].startswith("Windows 10") and win_ver[2] >= "Version 1903"
126-
)
116+
mscms = WCSManagerProxy()
127117

128118

129119
if TYPE_CHECKING:
@@ -2363,93 +2353,16 @@ def _wcs_get_display_profile(
23632353
None | str | ICCProfile: The display profile path as a string or
23642354
an ICCProfile object, or None if no profile is found.
23652355
"""
2366-
buf = ctypes.create_unicode_buffer(256)
2367-
_win10_1903_take_process_handles_snapshot()
2368-
retv = mscms.WcsGetDefaultColorProfile(
2369-
scope,
2370-
devicekey,
2371-
profile_type,
2372-
profile_subtype,
2373-
profile_id,
2374-
ctypes.sizeof(buf), # Bytes
2375-
ctypes.byref(buf),
2356+
prof = mscms.get_default_color_profile(
2357+
scope, devicekey, profile_type, profile_subtype, profile_id
23762358
)
2377-
_win10_1903_close_leaked_regkey_handles(devicekey)
2378-
if not retv:
2379-
raise util_win.get_windows_error(ctypes.windll.kernel32.GetLastError())
2380-
if buf.value:
2359+
if prof:
23812360
if path_only:
2382-
return os.path.join(ICCPROFILES[0], buf.value)
2383-
return ICCProfile(buf.value, use_cache=use_cache)
2361+
return os.path.join(ICCPROFILES[0], prof)
2362+
return ICCProfile(prof, use_cache=use_cache)
23842363
return None
23852364

23862365

2387-
def _win10_1903_take_process_handles_snapshot() -> None:
2388-
"""Take a snapshot of the current process handles under Win10 1903."""
2389-
global prev_handles
2390-
prev_handles = []
2391-
if win10_1903 and DEBUG:
2392-
try:
2393-
for handle in get_process_handles():
2394-
prev_handles.append(handle.HandleValue)
2395-
except OSError as exception:
2396-
print("Couldn't get process handles:", exception)
2397-
2398-
2399-
def _win10_1903_close_leaked_regkey_handles(devicekey: str) -> None:
2400-
"""Close leaked registry key handles under Win10 1903.
2401-
2402-
Args:
2403-
devicekey (str): The device key to match against leaked handles.
2404-
"""
2405-
global prev_handles
2406-
if not win10_1903:
2407-
return
2408-
# Wcs* methods leak handles under Win10 1903. Get and close them.
2409-
2410-
# Extract substring from devicekey for matching handle name, e.g.
2411-
# Control\Class\{4d36e96e-e325-11ce-bfc1-08002be10318}
2412-
substr = "\\".join(devicekey.split("\\")[-4:-1])
2413-
try:
2414-
handles = get_process_handles()
2415-
except OSError as exception:
2416-
print("Couldn't get process handles:", exception)
2417-
return
2418-
for handle in handles:
2419-
try:
2420-
handle_name = get_handle_name(handle)
2421-
except OSError as exception:
2422-
print(f"Couldn't get name of handle 0x{handle.HandleValue:x}:", exception)
2423-
handle_name = None
2424-
if DEBUG and handle.HandleValue not in prev_handles:
2425-
try:
2426-
handle_type = get_handle_type(handle)
2427-
except OSError as exception:
2428-
print(
2429-
f"Couldn't get typestring of handle 0x{handle.HandleValue:x}:",
2430-
exception,
2431-
)
2432-
handle_type = None
2433-
print(
2434-
"New handle",
2435-
f"0x{handle.HandleValue:x}",
2436-
f"type 0x{handle.ObjectTypeIndex:02x} {handle_type}",
2437-
handle_name,
2438-
)
2439-
if handle_name and handle_name.endswith(substr):
2440-
print(
2441-
"Windows 10",
2442-
win_ver[2].split(" ", 1)[-1],
2443-
f"housekeeping: Closing leaked handle 0x{handle.HandleValue:x}",
2444-
handle_name,
2445-
)
2446-
2447-
try:
2448-
win32api.RegCloseKey(handle.HandleValue)
2449-
except pywintypes.error as exception:
2450-
print(f"Couldn't close handle 0x{handle.HandleValue:x}:", exception)
2451-
2452-
24532366
def _winreg_get_display_profile(
24542367
monkey: list,
24552368
current_user: bool = False,
@@ -2901,26 +2814,8 @@ def _wcs_set_display_profile(
29012814
Returns:
29022815
bool: True if the profile was set successfully, False otherwise.
29032816
"""
2904-
# We need to disassociate the profile first in case it's not the default
2905-
# so we can make it the default again.
2906-
# Note that disassociating the current default profile for a display will
2907-
# also set its video card gamma ramps to linear if Windows calibration
2908-
# management isn't enabled.
2909-
_win10_1903_take_process_handles_snapshot()
2910-
with contextlib.suppress(WindowsError):
2911-
# Disassociate the profile from the device first
2912-
mscms.WcsDisassociateColorProfileFromDevice(scope, profile_name, devicekey)
2913-
try:
2914-
# Associate the profile with the device
2915-
retv = mscms.WcsAssociateColorProfileWithDevice(scope, profile_name, devicekey)
2916-
except OSError:
2917-
retv = None
2918-
_win10_1903_close_leaked_regkey_handles(devicekey)
2919-
if not retv:
2920-
raise util_win.get_windows_error(ctypes.windll.kernel32.GetLastError())
2921-
monkey = devicekey.split("\\")[-2:]
2922-
current_user = scope == WCS_PROFILE_MANAGEMENT_SCOPE["CURRENT_USER"]
2923-
profiles = _winreg_get_display_profiles(monkey, current_user)
2817+
mscms.associate_color_profile_with_device(scope, profile_name, str(devicekey))
2818+
profiles = mscms.get_device_color_profile_list(scope, str(devicekey))
29242819
return profile_name in profiles
29252820

29262821

@@ -2949,38 +2844,9 @@ def _wcs_unset_display_profile(
29492844
Returns:
29502845
bool: True if the profile was unset successfully, False otherwise.
29512846
"""
2952-
# Disassociating a profile will always (regardless of whether or
2953-
# not the profile was associated or even exists) result in Windows
2954-
# error code 2015 ERROR_PROFILE_NOT_ASSOCIATED_WITH_DEVICE.
2955-
# This is probably a Windows bug.
2956-
# To have a meaningful return value, we thus check wether the profile that
2957-
# should be removed is currently associated, and only fail if it is not,
2958-
# or if disassociating it fails for some reason.
2959-
monkey = devicekey.split("\\")[-2:]
2960-
current_user = scope == WCS_PROFILE_MANAGEMENT_SCOPE["CURRENT_USER"]
2961-
profiles = _winreg_get_display_profiles(monkey, current_user)
2962-
_win10_1903_take_process_handles_snapshot()
2963-
try:
2964-
# Disassociate the profile from the device
2965-
retv = mscms.WcsDisassociateColorProfileFromDevice(
2966-
scope, profile_name, devicekey
2967-
)
2968-
except OSError:
2969-
retv = None
2970-
_win10_1903_close_leaked_regkey_handles(devicekey)
2971-
if not retv:
2972-
errcode = ctypes.windll.kernel32.GetLastError()
2973-
if (
2974-
errcode in (ERROR_PROFILE_NOT_ASSOCIATED_WITH_DEVICE, ERROR_SUCCESS)
2975-
and profile_name in profiles
2976-
):
2977-
# Check if profile is still associated
2978-
profiles = _winreg_get_display_profiles(monkey, current_user)
2979-
if profile_name not in profiles:
2980-
# Successfully disassociated
2981-
return True
2982-
raise util_win.get_windows_error(errcode)
2983-
return True
2847+
mscms.disassociate_color_profile_from_device(scope, profile_name, str(devicekey))
2848+
profiles = mscms.get_device_color_profile_list(scope, str(devicekey))
2849+
return profile_name not in profiles
29842850

29852851

29862852
def set_display_profile(

0 commit comments

Comments
 (0)