File: util_win.py

package info (click to toggle)
displaycal-py3 3.9.16-1
  • links: PTS
  • area: main
  • in suites: forky, sid, trixie
  • size: 29,120 kB
  • sloc: python: 115,777; javascript: 11,540; xml: 598; sh: 257; makefile: 173
file content (809 lines) | stat: -rw-r--r-- 25,425 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
# -*- coding: utf-8 -*-
"""This module provides utility functions for interacting with Windows-specific
features, such as display devices, process management, and color management.
"""

import ctypes
from ctypes import POINTER, byref, sizeof, windll, wintypes
from ctypes.wintypes import DWORD, HANDLE, LPWSTR
import _ctypes
import platform
import struct
import sys
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING


import pywintypes

import win32api

from win32comext.shell import shell as win32com_shell

import win32con

import win32process

import winerror
import winreg

from DisplayCAL.util_os import quote_args
from DisplayCAL.win_structs import UNICODE_STRING


if TYPE_CHECKING:
    from _win32typing import PyDISPLAY_DEVICE


if not hasattr(ctypes, "c_bool"):
    # Python 2.5
    ctypes.c_bool = ctypes.c_int

if sys.getwindowsversion() >= (6,):
    LPDWORD = POINTER(DWORD)
    PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
    kernel32 = windll.kernel32
    QueryFullProcessImageNameW = kernel32.QueryFullProcessImageNameW
    QueryFullProcessImageNameW.argtypes = [HANDLE, DWORD, LPWSTR, LPDWORD]
    QueryFullProcessImageNameW.restype = bool

try:
    psapi = ctypes.windll.psapi
except WindowsError:
    psapi = None


# Access registry directly instead of Wcs* functions that leak handles
USE_REGISTRY = True


# DISPLAY_DEVICE structure, StateFlags member
# http://msdn.microsoft.com/en-us/library/dd183569%28v=vs.85%29.aspx

# wingdi.h

# Flags for parent devices
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 0x1
DISPLAY_DEVICE_MIRRORING_DRIVER = 0x8
"""Represents a pseudo device used to mirror application drawing for remoting or
other purposes.

An invisible pseudo monitor is associated with this device.

For example, NetMeeting uses it.
Note that GetSystemMetrics (SM_MONITORS) only accounts for visible display monitors.
"""
DISPLAY_DEVICE_MODESPRUNED = (
    0x8000000  # The device has more display modes than its output devices support.
)
DISPLAY_DEVICE_PRIMARY_DEVICE = 0x4
"""The primary desktop is on the device.
For a system with a single display card, this is always set.
For a system with multiple display cards, only one device can have this set.
"""
DISPLAY_DEVICE_REMOVABLE = (
    0x20  # The device is removable; it cannot be the primary display.
)
DISPLAY_DEVICE_VGA_COMPATIBLE = 0x10  # The device is VGA compatible.
DISPLAY_DEVICE_DISCONNECT = 0x2000000
DISPLAY_DEVICE_MULTI_DRIVER = 0x2
DISPLAY_DEVICE_REMOTE = 0x4000000

# Flags for child devices
DISPLAY_DEVICE_ACTIVE = 0x1
"""DISPLAY_DEVICE_ACTIVE specifies whether a monitor is presented as being "on"
by the respective GDI view.

Windows Vista:
EnumDisplayDevices will only enumerate monitors that can be presented as being "on."
"""
DISPLAY_DEVICE_ATTACHED = 0x2


# MONITORINFO structure, dwFlags member
# https://msdn.microsoft.com/de-de/library/windows/desktop/dd145065(v=vs.85).aspx
MONITORINFOF_PRIMARY = 0x1

# Icm.h
CLASS_MONITOR = struct.unpack("!L", b"mntr")[0]
CLASS_PRINTER = struct.unpack("!L", b"prtr")[0]
CLASS_SCANNER = struct.unpack("!L", b"scnr")[0]

# ShellExecute
SEE_MASK_FLAG_NO_UI = 0x00000400
SEE_MASK_NOASYNC = 0x00000100
SEE_MASK_NOCLOSEPROCESS = 0x00000040
SEE_MASK_WAITFORINPUTIDLE = 0x02000000


def _get_icm_display_device_key(devicekey):
    monkey = devicekey.split("\\")[-2:]  # pun totally intended
    subkey = "\\".join(
        [
            "Software",
            "Microsoft",
            "Windows NT",
            "CurrentVersion",
            "ICM",
            "ProfileAssociations",
            "Display",
        ]
        + monkey
    )
    return winreg.CreateKey(winreg.HKEY_CURRENT_USER, subkey)


class MSCMSLoader:
    """Loader class for MSCMS."""

    _windll = None

    @classmethod
    def get_mscms_windll(cls):
        """Get the MSCMS windll instance.

        Returns:
            MSCMS: The MSCMS windll instance.
        """
        if not cls._windll:
            cls._windll = MSCMS()
        return cls._windll


def _get_mscms_windll():
    return MSCMSLoader.get_mscms_windll()


def calibration_management_isenabled() -> bool:
    """Check if calibration is enabled under Windows 7.

    Returns:
        bool: True if calibration is enabled, False otherwise.
    """
    if sys.getwindowsversion() < (6, 1):
        # Windows XP and Vista don't have calibration management
        return False
    if False:
        # Using registry - NEVER
        # Also, does not work!
        with winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE,
            r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ICM\Calibration",
        ) as key:
            return bool(winreg.QueryValueEx(key, "CalibrationManagementEnabled")[0])
    else:
        # Using ctypes
        mscms = _get_mscms_windll()
        pbool = ctypes.pointer(ctypes.c_bool())
        if not mscms or not mscms.WcsGetCalibrationManagementState(pbool):
            return
        return bool(pbool.contents)


def disable_calibration_management():
    """Disable calibration loading under Windows 7."""
    enable_calibration_management(False)


def disable_per_user_profiles(display_no: int = 0) -> None:
    """Disable per user profiles under Vista/Windows 7.

    Args:
        display_no (int): The display number.
    """
    enable_per_user_profiles(False, display_no)


def enable_calibration_management(enable: bool = True) -> bool:
    """Enable calibration loading under Windows 7.

    Args:
        enable (bool): Whether to enable calibration management.

    Raises:
        NotImplementedError: If the OS version is less than Windows 7.
        get_windows_error: If an error occurs while setting the calibration
            management state.

    Returns:
        bool: True if successful, False otherwise.
    """
    if sys.getwindowsversion() < (6, 1):
        raise NotImplementedError(
            "Calibration Management is only available in Windows 7 or later"
        )
    if False:
        # Using registry - NEVER
        # Also, does not work!
        with winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE,
            r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ICM\Calibration",
            winreg.KEY_SET_VALUE,
        ) as key:
            winreg.SetValueEx(
                key, "CalibrationManagementEnabled", 0, winreg.REG_DWORD, int(enable)
            )
    else:
        # Using ctypes (must be called with elevated permissions)
        mscms = _get_mscms_windll()
        if not mscms:
            return False
        if not mscms.WcsSetCalibrationManagementState(enable):
            raise get_windows_error(ctypes.windll.kernel32.GetLastError())
        return True


def enable_per_user_profiles(
    enable: bool = True, display_no: int = 0, devicekey: str = None
) -> bool:
    """Enable per user profiles under Vista/Windows 7.

    Args:
        enable (bool): Whether to enable per user profiles.
        display_no (int): The display number.
        devicekey (str): The device key.

    Raises:
        NotImplementedError: If the OS version is less than Windows Vista.
        get_windows_error: If an error occurs while setting the per user
            profiles state.

    Returns:
        bool: True if successful, False otherwise.
    """
    if sys.getwindowsversion() < (6,):
        # Windows XP doesn't have per-user profiles
        raise NotImplementedError(
            "Per-user profiles are only available in Windows Vista, 7 or later"
        )
    if not devicekey:
        device = get_display_device(display_no)
        if device:
            devicekey = device.DeviceKey
    if devicekey:
        if USE_REGISTRY:
            with _get_icm_display_device_key(devicekey) as key:
                winreg.SetValueEx(
                    key, "UsePerUserProfiles", 0, winreg.REG_DWORD, int(enable)
                )
        else:
            # Using ctypes - this leaks registry key handles internally in
            # WcsSetUsePerUserProfiles since Windows 10 1903
            mscms = _get_mscms_windll()
            if not mscms:
                return False
            if not mscms.WcsSetUsePerUserProfiles(
                str(devicekey), CLASS_MONITOR, enable
            ):
                raise get_windows_error(ctypes.windll.kernel32.GetLastError())
        return True


def get_display_devices(devicename: str) -> List["PyDISPLAY_DEVICE"]:
    r"""Get all display devices of an output (there can be several).

    Example usage:
        get_display_devices('\\\\.\\DISPLAY1')
        devicename = '\\\\.\\DISPLAYn' where n is a positive integer starting at 1.

    Args:
        devicename (str): The device name.

    Returns:
        List[PyDISPLAY_DEVICE]: List of display devices.
    """
    devices = []
    n = 0
    while True:
        try:
            devices.append(win32api.EnumDisplayDevices(devicename, n))
        except pywintypes.error:
            break
        n += 1
    return devices


def get_first_display_device(
    devicename: str, exception_cls: Exception = pywintypes.error
) -> "PyDISPLAY_DEVICE":
    """Get the first display of device <devicename>.

    Args:
        devicename (str): The device name.
        exception_cls (Exception): The exception class to catch.

    Returns:
        PyDISPLAY_DEVICE: The first display device.
    """
    try:
        return win32api.EnumDisplayDevices(devicename, 0)
    except exception_cls:
        pass


def get_active_display_device(
    devicename: str, devices: Optional[List["PyDISPLAY_DEVICE"]] = None
) -> "PyDISPLAY_DEVICE":
    r"""Get active display device of an output (there can only be one per output).

    Return value: display device object or None.

    Example usage:
        get_active_display_device('\\\\.\\DISPLAY1')
        devicename = '\\\\.\\DISPLAYn' where n is a positive integer starting at 1.

    Args:
        devicename (str): The device name.
        devices (Optional[List[PyDISPLAY_DEVICE]]): List of devices.

    Returns:
        PyDISPLAY_DEVICE: The active display device (display device object) or None.
    """
    if not devices:
        devices = get_display_devices(devicename)
    for device in devices:
        if device.StateFlags & DISPLAY_DEVICE_ACTIVE and (
            len(devices) == 1 or device.StateFlags & DISPLAY_DEVICE_ATTACHED
        ):
            return device


def get_active_display_devices(
    attrname: Optional[str] = None,
) -> List["PyDISPLAY_DEVICE"]:
    """Return active display devices.

    Args:
        attrname (Optional[str]): The attribute name to get from the display device.

    Returns:
        List[PyDISPLAY_DEVICE]: List of active display devices.
    """
    devices = []
    for moninfo in get_real_display_devices_info():
        device = get_active_display_device(moninfo["Device"])
        if device:
            if attrname:
                device = getattr(device, attrname)
            devices.append(device)
    return devices


def get_display_device(
    display_no: int = 0,
    use_active_display_device: bool = False,
    exception_cls: Exception = pywintypes.error,
) -> "PyDISPLAY_DEVICE":
    """Get the display device for a given display number.

    Args:
        display_no (int): The display number.
        use_active_display_device (bool): Whether to use the active display device.
        exception_cls (Exception): The exception class to catch.

    Returns:
        DisplayDevice: The display device.
    """
    # The ordering will work as long as Argyll continues using EnumDisplayMonitors
    monitors = get_real_display_devices_info()
    moninfo = monitors[display_no]
    if use_active_display_device:
        return get_active_display_device(moninfo["Device"])
    else:
        return get_first_display_device(moninfo["Device"], exception_cls)


def get_process_filename(pid: int, handle: int = 0) -> str:
    """Get the filename of a process.

    Args:
        pid (int): The process ID.
        handle (int): The process handle.

    Raises:
        WinError: If an error occurs while querying the process filename.

    Returns:
        str: The filename of the process.
    """
    if sys.getwindowsversion() >= (6,):
        flags = PROCESS_QUERY_LIMITED_INFORMATION
    else:
        flags = win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ
    if not handle:
        handle = win32api.OpenProcess(flags, False, pid)
    try:
        if sys.getwindowsversion() >= (6,):
            dwSize = win32con.MAX_PATH
            while True:
                dwFlags = 0  # The name should use the Win32 path format
                lpdwSize = DWORD(dwSize)
                lpExeName = ctypes.create_unicode_buffer("", lpdwSize.value + 1)
                success = QueryFullProcessImageNameW(
                    int(handle), dwFlags, lpExeName, byref(lpdwSize)
                )
                if success and 0 < lpdwSize.value < dwSize:
                    break
                error = kernel32.GetLastError()
                if error != winerror.ERROR_INSUFFICIENT_BUFFER:
                    raise ctypes.WinError(error)
                dwSize = dwSize + 256
                if dwSize > 0x1000:
                    # This prevents an infinite loop under Windows Server 2008
                    # if the path contains spaces, see
                    # http://msdn.microsoft.com/en-us/library/ms684919(VS.85).aspx#4
                    raise ctypes.WinError(error)
            filename = lpExeName.value
        else:
            filename = win32process.GetModuleFileNameEx(handle, 0)
    finally:
        win32api.CloseHandle(handle)
    return filename


def get_file_info(filename: str) -> Dict:
    """Get exe/dll file information.

    Args:
        filename (str): The filename.

    Returns:
        Dict: The file information.
    """
    info = {"FileInfo": None, "StringFileInfo": {}, "FileVersion": None}

    finfo = win32api.GetFileVersionInfo(filename, "\\")
    info["FileInfo"] = finfo
    info["FileVersion"] = "{:d}.{:d}.{:d}.{:d}".format(
        finfo["FileVersionMS"] / 65536,
        finfo["FileVersionMS"] % 65536,
        finfo["FileVersionLS"] / 65536,
        finfo["FileVersionLS"] % 65536,
    )
    for lcid, codepage in win32api.GetFileVersionInfo(
        filename, "\\VarFileInfo\\Translation"
    ):
        info["StringFileInfo"][lcid, codepage] = {}
        for name in [
            "Comments",
            "CompanyName",
            "FileDescription",
            "FileVersion",
            "InternalName",
            "LegalCopyright",
            "LegalTrademarks",
            "OriginalFilename",
            "PrivateBuild",
            "ProductName",
            "ProductVersion",
            "SpecialBuild",
        ]:
            value = win32api.GetFileVersionInfo(
                filename, f"\\StringFileInfo\\{lcid:04X}{codepage:04X}\\{name}"
            )
            if value is not None:
                info["StringFileInfo"][lcid, codepage][name] = value

    return info


def get_pids() -> List[int]:
    """Get PIDs of all running processes.

    Raises:
        ImportError: If the psapi module is not available.
        get_windows_error: If an error occurs while enumerating processes.

    Returns:
        List[int]: List of PIDs.
    """
    if psapi is None:
        raise ImportError(
            "psapi module is not available. Please ensure it is installed and accessible."  # noqa: B950
        )
    pids_count = 1024
    while True:
        pids = (DWORD * pids_count)()
        pids_size = sizeof(pids)
        bytes = DWORD()
        if not psapi.EnumProcesses(byref(pids), pids_size, byref(bytes)):
            raise get_windows_error(ctypes.windll.kernel32.GetLastError())
        if bytes.value >= pids_size:
            pids_count *= 2
            continue
        count = bytes.value / (pids_size / pids_count)
        return [_f for _f in pids[:count] if _f]


def get_real_display_devices_info() -> List[Dict]:
    """Return info for real (non-virtual) devices.

    Returns:
        List[Dict]: List of monitor info.
    """
    # See Argyll source spectro/dispwin.c MonitorEnumProc, get_displays
    monitors = []
    for monitor in win32api.EnumDisplayMonitors(None, None):
        try:
            # Convert PyHANDLE to int using the .handle attribute
            hMonitor = monitor[0].handle
            moninfo = win32api.GetMonitorInfo(hMonitor)
        except pywintypes.error:
            pass
        else:
            if moninfo and not moninfo["Device"].startswith("\\\\.\\DISPLAYV"):
                monitors.append(moninfo)
    return monitors


def get_windows_error(errorcode: int) -> ctypes.WinError:
    """Get a Windows error message.

    Args:
        errorcode (int): The error code.

    Returns:
        ctypes.WinError: The Windows error message.
    """
    return ctypes.WinError(errorcode)


def per_user_profiles_isenabled(
    display_no: int = 0, devicekey: Optional[str] = None
) -> bool:
    """Check if per user profiles is enabled under Vista/Windows 7.

    Args:
        display_no (int): The display number.
        devicekey (str): The device key.

    Raises:
        WindowsError: If an error occurs while querying the registry.

    Returns:
        bool: True if per user profiles is enabled, False otherwise.
    """
    if sys.getwindowsversion() < (6,):
        # Windows XP doesn't have per-user profiles
        return False
    if not devicekey:
        device = get_display_device(display_no)
        if device:
            devicekey = device.DeviceKey
    if devicekey:
        if USE_REGISTRY:
            with _get_icm_display_device_key(devicekey) as key:
                try:
                    return bool(winreg.QueryValueEx(key, "UsePerUserProfiles")[0])
                except WindowsError as exception:
                    if exception.args[0] == winerror.ERROR_FILE_NOT_FOUND:
                        return False
                    raise
        else:
            # Using ctypes - this leaks registry key handles internally in
            # WcsGetUsePerUserProfiles since Windows 10 1903
            mscms = _get_mscms_windll()
            pbool = ctypes.pointer(ctypes.c_bool())
            if not mscms or not mscms.WcsGetUsePerUserProfiles(
                str(devicekey), CLASS_MONITOR, pbool
            ):
                return
            return bool(pbool.contents)


def run_as_admin(
    cmd: str,
    args: List[Any],
    close_process: bool = True,
    async_: bool = False,
    wait_for_idle: bool = False,
    show: bool = True,
) -> Dict:
    """Run command with elevated privileges.

    This is a wrapper around ShellExecuteEx.

    Args:
        cmd (str): The command to run.
        args (List[Any]): The arguments for the command.
        close_process (bool): Whether to close the process after execution.
        async_ (bool): Whether to run the command asynchronously.
        wait_for_idle (bool): Whether to wait for the process to be idle.
        show (bool): Whether to show the command window.

    Returns:
        Dict: A dictionary with hInstApp and hProcess members.
    """
    return shell_exec(cmd, args, "runas", close_process, async_, wait_for_idle, show)


def shell_exec(
    filename: str,
    args: List[Any],
    operation: str = "open",
    close_process: bool = True,
    async_: bool = False,
    wait_for_idle: bool = False,
    show: bool = True,
) -> Dict:
    """Run command.

    This is a wrapper around ShellExecuteEx.

    Args:
        filename (str): The filename to execute.
        args (list): The arguments for the command.
        operation (str): The operation to perform.
        close_process (bool): Whether to close the process after execution.
        async_ (bool): Whether to run the command asynchronously.
        wait_for_idle (bool): Whether to wait for the process to be idle.
        show (bool): Whether to show the command window.

    Returns:
        Dict: A dictionary with hInstApp and hProcess members.
    """
    flags = SEE_MASK_FLAG_NO_UI
    if not close_process:
        flags |= SEE_MASK_NOCLOSEPROCESS
    if not async_:
        flags |= SEE_MASK_NOASYNC
    if wait_for_idle:
        flags |= SEE_MASK_WAITFORINPUTIDLE
    params = " ".join(quote_args(args))
    if show:
        show = win32con.SW_SHOWNORMAL
    else:
        show = win32con.SW_HIDE
    return win32com_shell.ShellExecuteEx(
        fMask=flags, lpVerb=operation, lpFile=filename, lpParameters=params, nShow=show
    )


def win_ver() -> Tuple[str, int, str, str]:
    """Get Windows version info.

    Returns:
        Tuple[str, int, str, str]: A tuple containing the product name, CSD
            version, release, and build.
    """
    csd = sys.getwindowsversion()[-1]
    # Use the registry to get product name, e.g. 'Windows 7 Ultimate'.
    # Not recommended, but we don't care.
    pname = "Windows"
    release = ""
    build = ""
    key = None
    sam = winreg.KEY_READ
    if platform.machine() == "AMD64":
        sam |= winreg.KEY_WOW64_64KEY
    try:
        key = winreg.OpenKeyEx(
            winreg.HKEY_LOCAL_MACHINE,
            r"SOFTWARE\Microsoft\Windows NT\CurrentVersion",
            0,
            sam,
        )
        pname = winreg.QueryValueEx(key, "ProductName")[0]
        build = f"Build {winreg.QueryValueEx(key, 'CurrentBuildNumber')[0]}"
        # Since Windows 10
        release = f"Version {winreg.QueryValueEx(key, 'ReleaseId')[0]}"
        build += f".{winreg.QueryValueEx(key, 'UBR')[0]}"
    except Exception:
        pass
    finally:
        if key:
            winreg.CloseKey(key)
    return pname, csd, release, build


USE_NTDLL_LDR = False


def _free_library(handle):
    if USE_NTDLL_LDR:
        fn = ctypes.windll.ntdll.LdrUnloadDll
    else:
        fn = _ctypes.FreeLibrary
    fn(handle)


class UnloadableWinDLL:
    """WinDLL wrapper that allows unloading."""

    def __init__(self, dllname):
        self.dllname = dllname
        self._windll = None
        self.load()

    def __getattr__(self, name: str) -> Any:
        """Get an attribute from the loaded DLL.

        Args:
            name (str): The name of the attribute.

        Returns:
            The attribute from the loaded DLL.
        """
        self.load()
        return getattr(self._windll, name)

    def __bool__(self) -> bool:
        """Check if the DLL is loaded.

        Returns:
            bool: True if the DLL is loaded, False otherwise.
        """
        self.load()
        return bool(self._windll)

    def load(self) -> None:
        """Load the DLL."""
        if not self._windll:
            if USE_NTDLL_LDR:
                mod = byref(UNICODE_STRING(len(self.dllname) * 2, 256, self.dllname))
                handle = wintypes.HANDLE()
                ctypes.windll.ntdll.LdrLoadDll(None, 0, mod, byref(handle))
                windll = ctypes.WinDLL(self.dllname, handle=handle.value)
            else:
                windll = ctypes.WinDLL(self.dllname)
            self._windll = windll

    def unload(self) -> None:
        """Unload the DLL."""
        if self._windll:
            handle = self._windll._handle
            self._windll = None
            _free_library(handle)


class MSCMS(UnloadableWinDLL):
    """MSCMS wrapper (optionally) allowing unloading."""

    def __init__(self, bootstrap_icm32=False) -> None:
        self._icm32_handle = None
        UnloadableWinDLL.__init__(self, "mscms.dll")
        if bootstrap_icm32:
            # Need to load & unload icm32 once before unloading of mscms can
            # work in every situation (some calls to mscms methods pull in
            # icm32, if we haven't loaded/unloaded it before, we won't be able
            # to unload then)
            self._icm32_handle = ctypes.WinDLL("icm32")._handle
            _free_library(self._icm32_handle)

    def load(self) -> None:
        """Load the MSCMS DLL."""
        mscms = self._windll
        UnloadableWinDLL.load(self)
        if self._windll is not mscms:
            mscms = self._windll
            if mscms:  # Ensure mscms is not None
                mscms.WcsGetDefaultColorProfileSize.restype = ctypes.c_bool
                mscms.WcsGetDefaultColorProfile.restype = ctypes.c_bool
                mscms.WcsAssociateColorProfileWithDevice.restype = ctypes.c_bool
                mscms.WcsDisassociateColorProfileFromDevice.restype = ctypes.c_bool

    def unload(self) -> None:
        """Unload the MSCMS DLL.

        Raises:
            WindowsError: If an error occurs while unloading the DLL.
        """
        if self._windll:
            if self._icm32_handle:
                # Need to free icm32 first, otherwise mscms won't unload
                try:
                    _free_library(self._icm32_handle)
                except WindowsError as exception:
                    if exception.args[0] != winerror.ERROR_MOD_NOT_FOUND:
                        raise
            UnloadableWinDLL.unload(self)


if __name__ == "__main__":
    if "calibration" in sys.argv[1:]:
        if "enable" in sys.argv[1:] or "disable" in sys.argv[1:]:
            enable_calibration_management(sys.argv[1:][-1] != "disable")
        print(calibration_management_isenabled())
    elif "per_user_profiles" in sys.argv[1:]:
        if "enable" in sys.argv[1:] or "disable" in sys.argv[1:]:
            enable_per_user_profiles(sys.argv[1:][-1] != "disable")
        print(per_user_profiles_isenabled())