Skip to content
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

Scheme handler: Windows scheme handler does not need admin privileges #177

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 0 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,6 @@ Usage of shim:

Shim is installed with AYON launcher, or on first run of AYON launcher. It is installed to app data which is based on OS, or to `{AYON_LAUNCHER_LOCAL_DIR}/shim/` directory if `AYON_LAUNCHER_LOCAL_DIR` environment variable is set.

Windows installation requires admin privileges, you'll be prompted for them on installation of AYON launcher.

<details>

<summary>Windows registry</summary>

You can register custom uri protocol scheme `ayon-launcher://` by adding following registry keys:
It uses default shim executable path `%LOCALAPPDATA%\Ynput\AYON\shim\ayon.exe`.

```
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\ayon-launcher]
"URL Protocol"=""

[HKEY_CLASSES_ROOT\ayon-launcher\DefaultIcon]
@=hex(2):25,00,4c,00,4f,00,43,00,41,00,4c,00,41,00,50,00,50,00,44,00,41,00,54,\
00,41,00,25,00,5c,00,59,00,6e,00,70,00,75,00,74,00,5c,00,41,00,59,00,4f,00,\
4e,00,5c,00,73,00,68,00,69,00,6d,00,5c,00,61,00,79,00,6f,00,6e,00,2e,00,65,\
00,78,00,65,00,00,00

[HKEY_CLASSES_ROOT\ayon-launcher\shell]

[HKEY_CLASSES_ROOT\ayon-launcher\shell\open]

[HKEY_CLASSES_ROOT\ayon-launcher\shell\open\command]
@=hex(2):25,00,4c,00,4f,00,43,00,41,00,4c,00,41,00,50,00,50,00,44,00,41,00,54,\
00,41,00,25,00,5c,00,59,00,6e,00,70,00,75,00,74,00,5c,00,41,00,59,00,4f,00,\
4e,00,5c,00,73,00,68,00,69,00,6d,00,5c,00,61,00,79,00,6f,00,6e,00,2e,00,65,\
00,78,00,65,00,20,00,22,00,25,00,31,00,22,00,00,00

```

</details>

> [!CAUTION]
> Shim on macOs is installed to `/Applications/AYON.app`. To be able to use it in automation which waits for AYON launcher to finish then you have to use full path to executable `/Applications/AYON.app/Contents/MacOS/ayon`.

Expand Down
112 changes: 27 additions & 85 deletions common/ayon_common/_windows_register_scheme.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import os
import sys
import json
import subprocess
import tempfile

import winreg

SCRIPT_PATH = os.path.abspath(__file__)
PROTOCOL_NAME = "ayon-launcher"
_REG_ICON_PATH = "\\".join([PROTOCOL_NAME, "DefaultIcon"])
_REG_COMMAND_PATH = "\\".join([PROTOCOL_NAME, "shell", "open", "command"])
USER_CLASSES_PATH = "\\".join(["SOFTWARE", "Classes"])
PROTOCOL_PATH = "\\".join([USER_CLASSES_PATH, PROTOCOL_NAME])
_REG_ICON_PATH = "\\".join([PROTOCOL_PATH, "DefaultIcon"])
_REG_COMMAND_PATH = "\\".join([PROTOCOL_PATH, "shell", "open", "command"])


def _reg_exists(root, path):
Expand All @@ -26,69 +25,18 @@ def _reg_exists(root, path):
return False


def _update_reg_in_subprocess(shim_icon_path, shim_command):
import win32con
import win32process
import win32event
import pywintypes
from win32comext.shell.shell import ShellExecuteEx
from win32comext.shell import shellcon

with tempfile.NamedTemporaryFile(
prefix="ayon_reg", mode="w", delete=False
) as tmp:
tmp.write(json.dumps({
"icon": shim_icon_path,
"command": shim_command
}))
tmp_path = tmp.name

executable = sys.executable
command = subprocess.list2cmdline(
["--skip-bootstrap", SCRIPT_PATH, tmp_path]
)
try:
process_info = ShellExecuteEx(
nShow=win32con.SW_SHOWNORMAL,
fMask=shellcon.SEE_MASK_NOCLOSEPROCESS,
lpVerb="runas",
lpFile=executable,
lpParameters=command,
lpDirectory=os.path.dirname(executable)
def _update_reg(shim_icon_path, shim_command):
with winreg.CreateKeyEx(
winreg.HKEY_CURRENT_USER,
PROTOCOL_PATH,
access=winreg.KEY_WRITE
) as key:
winreg.SetValueEx(
key, "URL Protocol", 0, winreg.REG_SZ, ""
)

except pywintypes.error:
# User cancelled UAC dialog
return False

process_handle = process_info["hProcess"]
win32event.WaitForSingleObject(process_handle, win32event.INFINITE)
returncode = win32process.GetExitCodeProcess(process_handle)
if returncode != 0:
return False

with open(tmp_path, "r") as stream:
data = json.load(stream)
return data.get("success", False)


def _update_reg(shim_icon_path, shim_command, force_current_process=False):
try:
with winreg.CreateKeyEx(
winreg.HKEY_CLASSES_ROOT,
PROTOCOL_NAME,
access=winreg.KEY_WRITE
) as key:
winreg.SetValueEx(
key, "URL Protocol", 0, winreg.REG_SZ, ""
)
except PermissionError:
if force_current_process:
raise RuntimeError("Failed to set registry keys")
return _update_reg_in_subprocess(shim_icon_path, shim_command)

with winreg.CreateKeyEx(
winreg.HKEY_CLASSES_ROOT,
winreg.HKEY_CURRENT_USER,
_REG_ICON_PATH,
access=winreg.KEY_WRITE
) as key:
Expand All @@ -97,7 +45,7 @@ def _update_reg(shim_icon_path, shim_command, force_current_process=False):
)

with winreg.CreateKeyEx(
winreg.HKEY_CLASSES_ROOT,
winreg.HKEY_CURRENT_USER,
_REG_COMMAND_PATH,
access=winreg.KEY_WRITE
) as key:
Expand All @@ -108,18 +56,24 @@ def _update_reg(shim_icon_path, shim_command, force_current_process=False):


def _needs_update(shim_icon_path, shim_command):
# If 'USER_CLASSES_PATH' is not available, then the protocol
# is not registered. Probably running as service.
# 'HKEY_CURRENT_USER\SOFTWARE\Classes'
if not _reg_exists(winreg.HKEY_CURRENT_USER, USER_CLASSES_PATH):
return False

# Validate existence of all required registry keys
if (
not _reg_exists(winreg.HKEY_CLASSES_ROOT, PROTOCOL_NAME)
or not _reg_exists(winreg.HKEY_CLASSES_ROOT, _REG_ICON_PATH)
or not _reg_exists(winreg.HKEY_CLASSES_ROOT, _REG_COMMAND_PATH)
not _reg_exists(winreg.HKEY_CURRENT_USER, PROTOCOL_PATH)
or not _reg_exists(winreg.HKEY_CURRENT_USER, _REG_ICON_PATH)
or not _reg_exists(winreg.HKEY_CURRENT_USER, _REG_COMMAND_PATH)
):
return True

# Check if protocol has set version
with winreg.OpenKey(
winreg.HKEY_CLASSES_ROOT,
PROTOCOL_NAME,
winreg.HKEY_CURRENT_USER,
PROTOCOL_PATH,
0,
winreg.KEY_READ
) as key:
Expand All @@ -128,7 +82,7 @@ def _needs_update(shim_icon_path, shim_command):
return True

with winreg.OpenKey(
winreg.HKEY_CLASSES_ROOT,
winreg.HKEY_CURRENT_USER,
_REG_ICON_PATH,
0,
winreg.KEY_READ
Expand All @@ -141,7 +95,7 @@ def _needs_update(shim_icon_path, shim_command):
return True

with winreg.OpenKey(
winreg.HKEY_CLASSES_ROOT,
winreg.HKEY_CURRENT_USER,
_REG_COMMAND_PATH,
0,
winreg.KEY_READ
Expand Down Expand Up @@ -175,15 +129,3 @@ def set_reg(shim_path: str) -> bool:
if _needs_update(shim_icon_path, shim_command):
return _update_reg(shim_icon_path, shim_command)
return True


if __name__ == "__main__":
json_path = sys.argv.pop(-1)
with open(json_path, "r") as stream:
data = json.load(stream)
shim_command = data["command"]
shim_icon_path = data["icon"]
_update_reg(shim_icon_path, shim_command, True)
data["success"] = True
with open(json_path, "w") as stream:
json.dump(data, stream)