From 2f0c31539afa9bf1942b85bf4b02b09f5dca1854 Mon Sep 17 00:00:00 2001 From: vkt1414 Date: Tue, 9 Jul 2024 07:03:33 -0400 Subject: [PATCH 01/14] add IDCRequestHandler --- IDCBrowser/IDCRequestHandler.py | 176 ++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 IDCBrowser/IDCRequestHandler.py diff --git a/IDCBrowser/IDCRequestHandler.py b/IDCBrowser/IDCRequestHandler.py new file mode 100644 index 0000000..6f13f5c --- /dev/null +++ b/IDCBrowser/IDCRequestHandler.py @@ -0,0 +1,176 @@ +# slicer_server_utils.py + +import os +import subprocess +import time +import logging +import requests +from slicer.util import VTKObservationMixin +import qt +import slicer +import ctk +# IDCRequestHandler.py + +from WebServerLib.BaseRequestHandler import BaseRequestHandler, BaseRequestLoggingFunction +import urllib +import os +from typing import Optional +from idc_index import index + +# PORT = 2042 + +# def get_slicer_location(): +# launcherPath = qt.QDir.toNativeSeparators( +# qt.QFileInfo(slicer.app.launcherExecutableFilePath).absoluteFilePath() +# ) +# return launcherPath + +# def is_server_running(): +# try: +# response = requests.get(f"http://127.0.0.1:{PORT}/idc/collections", timeout=3) +# if 'applicationName' in response.json(): +# return True +# except Exception as e: +# logging.debug("Application is not available: " + str(e)) +# return False + +# def start_server(slicer_executable=None, timeoutSec=60): +# # if not slicer_executable: +# # if 'SLICER_EXECUTABLE' not in os.environ: +# # os.environ['SLICER_EXECUTABLE'] = get_slicer_location() +# # slicer_executable = get_slicer_location() +# # p = subprocess.Popen([slicer_executable, "--python-code", f"wslogic = getModuleLogic('WebServer'); wslogic.port={PORT}; wslogic.enableSlicer=False; wslogic.enableStaticPages=False; wslogic.enableDICOM=True; wslogic.requestHandlers = [IDCRequestHandler()], wslogic.start()"]) +# # start = time.time() +# # connected = False +# # while not connected: +# # connected = is_server_running() +# # if time.time() - start > timeoutSec: +# # raise requests.exceptions.ConnectTimeout("Timeout while waiting for application to start") +# # return p +# if not is_server_running(): + +# The following part remains unchanged +# PORT = 2042 +# import WebServer + +# try: +# logic.stop() +# except NameError: +# pass + +# logMessage = WebServer.WebServerLogic.defaultLogMessage +# requestHandlers = [IDCRequestHandler()] +# logic = WebServer.WebServerLogic(port=PORT, logMessage=logMessage, enableSlicer=True, enableStaticPages=False, enableDICOM=False, requestHandlers=requestHandlers) + +# logic.start() + +import os +import subprocess +import time +import logging +import requests +from slicer.util import VTKObservationMixin +import qt +import slicer +import ctk +# IDCRequestHandler.py + +from WebServerLib.BaseRequestHandler import BaseRequestHandler, BaseRequestLoggingFunction +import urllib +import os +from typing import Optional +from idc_index import index + +class IDCRequestHandler(BaseRequestHandler): + + def __init__(self, logMessage: Optional[BaseRequestLoggingFunction] = None): + self.logMessage = logMessage + self.client = index.IDCClient() + self.index_df = self.client.index + + # if not is_server_running(): + # start_server() + + def canHandleRequest(self, uri: bytes, **_kwargs) -> float: + parsedURL = urllib.parse.urlparse(uri) + if parsedURL.path.startswith(b"/idc"): + return 0.5 + return 0.0 + + def handleRequest(self, method: str, uri: bytes, requestBody: bytes, **_kwargs) -> tuple[bytes, bytes]: + parsedURL = urllib.parse.urlparse(uri) + splitPath = parsedURL.path.split(b"/") + + if len(splitPath) > 2: + if splitPath[2] == b"collections": + return self.handleCollections() + elif splitPath[2] == b"download" and splitPath[3] == b"seriesInstanceUID": + series_uids = splitPath[4].decode().split(",") + return self.handleDownload(series_uids) + elif splitPath[2] == b"download" and splitPath[3] == b"studyInstanceUID": + study_uids = splitPath[4].decode().split(",") + filtered_df = self.index_df[self.index_df['StudyInstanceUID'].isin(study_uids)] + series_uids_from_study_uid = filtered_df['SeriesInstanceUID'].tolist() + return self.handleDownload(series_uids_from_study_uid) + else: + return b"text/plain", b"Unhandled IDC request path" + else: + return b"text/plain", b"Invalid IDC request path" + + def handleCollections(self) -> tuple[bytes, bytes]: + try: + collections = self.client.get_collections() + responseBody = f"Available collections: {', '.join(collections)}".encode() + contentType = b"text/plain" + except Exception as e: + responseBody = f"Error fetching collections: {e}".encode() + contentType = b"text/plain" + if self.logMessage: + self.logMessage(responseBody.decode()) + + return contentType, responseBody + + def handleDownload(self, uids: list[str]) -> tuple[bytes, bytes]: + destFolderPath = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() + + try: + self.client.download_from_selection(seriesInstanceUID=uids, downloadDir=destFolderPath, dirTemplate="%SeriesInstanceUID") + indexer = ctk.ctkDICOMIndexer() + for uid in uids: + download_folder_path = os.path.join(destFolderPath, uid) + indexer.addDirectory(slicer.dicomDatabase, download_folder_path) + plugin = slicer.modules.dicomPlugins["DICOMScalarVolumePlugin"]() + dicomDatabase = slicer.dicomDatabase + fileList = dicomDatabase.filesForSeries(uid.replace("'", "")) + loadables = plugin.examine([fileList]) + if len(loadables) > 0: + volume = plugin.load(loadables[0]) + logging.debug("Loaded volume: " + volume.GetName()) + else: + raise Exception("Unable to load DICOM content. Please retry from DICOM Browser!") + + responseBody = f"Downloaded and indexed UID(s): {', '.join(uids)}".encode() + contentType = b"text/plain" + except Exception as e: + responseBody = f"Error downloading or indexing UID(s): {e}".encode() + contentType = b"text/plain" + if self.logMessage: + self.logMessage(responseBody.decode()) + + return contentType, responseBody + + +# The following part remains unchanged +PORT = 2042 +import WebServer + +try: + logic.stop() +except NameError: + pass + +logMessage = WebServer.WebServerLogic.defaultLogMessage +requestHandlers = [IDCRequestHandler()] +logic = WebServer.WebServerLogic(port=PORT, logMessage=logMessage, enableSlicer=True, enableStaticPages=False, enableDICOM=True, requestHandlers=requestHandlers) + +logic.start() \ No newline at end of file From 8f7e03856719f4f145e722e9f7c25d7b05d6000c Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi <115020590+vkt1414@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:48:49 -0400 Subject: [PATCH 02/14] enh: use onStartupCompleted in scripted Loadable Module to register IDC request handler --- ...CRequestHandler.py => IDCHandlerModule.py} | 105 +++++------------- 1 file changed, 29 insertions(+), 76 deletions(-) rename IDCBrowser/{IDCRequestHandler.py => IDCHandlerModule.py} (61%) diff --git a/IDCBrowser/IDCRequestHandler.py b/IDCBrowser/IDCHandlerModule.py similarity index 61% rename from IDCBrowser/IDCRequestHandler.py rename to IDCBrowser/IDCHandlerModule.py index 6f13f5c..554de50 100644 --- a/IDCBrowser/IDCRequestHandler.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -1,68 +1,10 @@ -# slicer_server_utils.py - import os -import subprocess -import time -import logging -import requests -from slicer.util import VTKObservationMixin -import qt import slicer -import ctk -# IDCRequestHandler.py - -from WebServerLib.BaseRequestHandler import BaseRequestHandler, BaseRequestLoggingFunction -import urllib -import os +import urllib.parse +from slicer.ScriptedLoadableModule import * +from WebServer import WebServerLogic from typing import Optional -from idc_index import index - -# PORT = 2042 - -# def get_slicer_location(): -# launcherPath = qt.QDir.toNativeSeparators( -# qt.QFileInfo(slicer.app.launcherExecutableFilePath).absoluteFilePath() -# ) -# return launcherPath - -# def is_server_running(): -# try: -# response = requests.get(f"http://127.0.0.1:{PORT}/idc/collections", timeout=3) -# if 'applicationName' in response.json(): -# return True -# except Exception as e: -# logging.debug("Application is not available: " + str(e)) -# return False - -# def start_server(slicer_executable=None, timeoutSec=60): -# # if not slicer_executable: -# # if 'SLICER_EXECUTABLE' not in os.environ: -# # os.environ['SLICER_EXECUTABLE'] = get_slicer_location() -# # slicer_executable = get_slicer_location() -# # p = subprocess.Popen([slicer_executable, "--python-code", f"wslogic = getModuleLogic('WebServer'); wslogic.port={PORT}; wslogic.enableSlicer=False; wslogic.enableStaticPages=False; wslogic.enableDICOM=True; wslogic.requestHandlers = [IDCRequestHandler()], wslogic.start()"]) -# # start = time.time() -# # connected = False -# # while not connected: -# # connected = is_server_running() -# # if time.time() - start > timeoutSec: -# # raise requests.exceptions.ConnectTimeout("Timeout while waiting for application to start") -# # return p -# if not is_server_running(): - -# The following part remains unchanged -# PORT = 2042 -# import WebServer - -# try: -# logic.stop() -# except NameError: -# pass - -# logMessage = WebServer.WebServerLogic.defaultLogMessage -# requestHandlers = [IDCRequestHandler()] -# logic = WebServer.WebServerLogic(port=PORT, logMessage=logMessage, enableSlicer=True, enableStaticPages=False, enableDICOM=False, requestHandlers=requestHandlers) - -# logic.start() +from WebServerLib.BaseRequestHandler import BaseRequestHandler, BaseRequestLoggingFunction import os import subprocess @@ -81,6 +23,7 @@ from typing import Optional from idc_index import index + class IDCRequestHandler(BaseRequestHandler): def __init__(self, logMessage: Optional[BaseRequestLoggingFunction] = None): @@ -88,9 +31,6 @@ def __init__(self, logMessage: Optional[BaseRequestLoggingFunction] = None): self.client = index.IDCClient() self.index_df = self.client.index - # if not is_server_running(): - # start_server() - def canHandleRequest(self, uri: bytes, **_kwargs) -> float: parsedURL = urllib.parse.urlparse(uri) if parsedURL.path.startswith(b"/idc"): @@ -160,17 +100,30 @@ def handleDownload(self, uids: list[str]) -> tuple[bytes, bytes]: return contentType, responseBody -# The following part remains unchanged -PORT = 2042 -import WebServer +class IDCHandlerModule(ScriptedLoadableModule): + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "IDC Handler Module" + self.parent.categories = ["Examples"] + self.parent.contributors = ["Your Name (Your Organization)"] + self.parent.helpText = """This module registers an IDCRequestHandler to handle IDC-related requests.""" + self.parent.acknowledgementText = """This module was developed by Your Name, Your Organization.""" + + slicer.app.connect("startupCompleted()", self.onStartupCompleted) + + def onStartupCompleted(self): + print("SlicerStartupCompleted emitted") + + PORT = 2042 + try: + logic.stop() + except NameError: + pass -try: - logic.stop() -except NameError: - pass + logMessage = WebServerLogic.defaultLogMessage + requestHandlers = [IDCRequestHandler()] + logic = WebServerLogic(port=PORT, logMessage=logMessage, enableSlicer=True, enableStaticPages=False, enableDICOM=True, requestHandlers=requestHandlers) -logMessage = WebServer.WebServerLogic.defaultLogMessage -requestHandlers = [IDCRequestHandler()] -logic = WebServer.WebServerLogic(port=PORT, logMessage=logMessage, enableSlicer=True, enableStaticPages=False, enableDICOM=True, requestHandlers=requestHandlers) + logic.start() + print("IDC Request Handler has been registered and server started.") -logic.start() \ No newline at end of file From e36f90cd6fd10db6be175fbe3f77017a4064282c Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi Date: Tue, 16 Jul 2024 18:46:30 +0000 Subject: [PATCH 03/14] enh: let on startup create the py script and register protocol' --- IDCBrowser/IDCHandlerModule.py | 87 +++++++++++++++++++++++++++++--- IDCBrowser/Resources/resolver.py | 37 ++++++++++++++ 2 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 IDCBrowser/Resources/resolver.py diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index 554de50..39af574 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -7,6 +7,7 @@ from WebServerLib.BaseRequestHandler import BaseRequestHandler, BaseRequestLoggingFunction import os +import platform import subprocess import time import logging @@ -15,15 +16,8 @@ import qt import slicer import ctk -# IDCRequestHandler.py - -from WebServerLib.BaseRequestHandler import BaseRequestHandler, BaseRequestLoggingFunction -import urllib -import os -from typing import Optional from idc_index import index - class IDCRequestHandler(BaseRequestHandler): def __init__(self, logMessage: Optional[BaseRequestLoggingFunction] = None): @@ -33,7 +27,7 @@ def __init__(self, logMessage: Optional[BaseRequestLoggingFunction] = None): def canHandleRequest(self, uri: bytes, **_kwargs) -> float: parsedURL = urllib.parse.urlparse(uri) - if parsedURL.path.startswith(b"/idc"): + if (parsedURL.path.startswith(b"/idc")): return 0.5 return 0.0 @@ -126,4 +120,81 @@ def onStartupCompleted(self): logic.start() print("IDC Request Handler has been registered and server started.") + + self.writeResolverScript() + self.registerCustomProtocol() + + def writeResolverScript(self): + current_dir = os.path.dirname(os.path.realpath(__file__)) + resolver_script_path = os.path.join(current_dir,'Resources', 'resolver.py') + + resolver_script_content = '''import sys +import urllib.parse +import requests +import webbrowser + +def resolve_url(url): + # Parse the URL + parsed_url = urllib.parse.urlparse(url) + + # Remove the scheme (idcbrowser://) from the URL and split the path + path_parts = parsed_url.netloc.split('/') + parsed_url.path.split('/')[1:] + + # Check the first part of the path to determine the endpoint + if path_parts[0] == 'collections': + new_url = "http://localhost:2042/idc/collections" + # Open the new URL in a web browser + webbrowser.open(new_url) + elif path_parts[0] == 'series': + new_url = f"http://localhost:2042/idc/download/seriesInstanceUID/{path_parts[1]}" + elif path_parts[0] == 'studies': + new_url = f"http://localhost:2042/idc/download/studyInstanceUID/{path_parts[1]}" + else: + print(f"Unhandled path: {path_parts[0]}") + return + + # Make the request to the new URL + response = requests.get(new_url) + + # Print the response + print(response.text) + +if __name__ == "__main__": + # The URL is passed as the first argument + url = sys.argv[1] + + # Resolve the URL + resolve_url(url) +''' + + with open(resolver_script_path, 'w') as f: + f.write(resolver_script_content) + print(f"Resolver script written to {resolver_script_path}") + + def registerCustomProtocol(self): + if platform.system() == "Linux": + # Check if the protocol is already registered + if os.path.exists(os.path.expanduser("~/.local/share/applications/idcbrowser.desktop")): + print("IDC Browser URL protocol is already registered.") + return + + # Get the current directory + current_dir = os.path.dirname(os.path.realpath(__file__)) + python_script_path = os.path.join(current_dir, 'resolver.py') + + # Register IDC Browser URL protocol + with open(os.path.expanduser("~/.local/share/applications/idcbrowser.desktop"), "w") as f: + f.write(f"""[Desktop Entry] +Name=IDC Browser +Exec=python3 {python_script_path} %u +Type=Application +Terminal=false +MimeType=x-scheme-handler/idcbrowser; +""") + + # Update MIME database + os.system("update-desktop-database ~/.local/share/applications/") + os.system("xdg-mime default idcbrowser.desktop x-scheme-handler/idcbrowser") + else: + print("IDC Browser URL protocol registration is not supported on this operating system.") diff --git a/IDCBrowser/Resources/resolver.py b/IDCBrowser/Resources/resolver.py new file mode 100644 index 0000000..704fcc4 --- /dev/null +++ b/IDCBrowser/Resources/resolver.py @@ -0,0 +1,37 @@ +import sys +import urllib.parse +import requests +import webbrowser + +def resolve_url(url): + # Parse the URL + parsed_url = urllib.parse.urlparse(url) + + # Remove the scheme (idcbrowser://) from the URL and split the path + path_parts = parsed_url.netloc.split('/') + parsed_url.path.split('/')[1:] + + # Check the first part of the path to determine the endpoint + if path_parts[0] == 'collections': + new_url = "http://localhost:2042/idc/collections" + # Open the new URL in a web browser + webbrowser.open(new_url) + elif path_parts[0] == 'series': + new_url = f"http://localhost:2042/idc/download/seriesInstanceUID/{path_parts[1]}" + elif path_parts[0] == 'studies': + new_url = f"http://localhost:2042/idc/download/studyInstanceUID/{path_parts[1]}" + else: + print(f"Unhandled path: {path_parts[0]}") + return + + # Make the request to the new URL + response = requests.get(new_url) + + # Print the response + print(response.text) + +if __name__ == "__main__": + # The URL is passed as the first argument + url = sys.argv[1] + + # Resolve the URL + resolve_url(url) From 2e95e5e808a971e162ac01cbf7195c548924a6be Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi <115020590+vkt1414@users.noreply.github.com> Date: Tue, 16 Jul 2024 19:39:48 -0400 Subject: [PATCH 04/14] enh: add windows registration handler --- IDCBrowser/IDCHandlerModule.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index 39af574..2c543f8 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -195,6 +195,34 @@ def registerCustomProtocol(self): # Update MIME database os.system("update-desktop-database ~/.local/share/applications/") os.system("xdg-mime default idcbrowser.desktop x-scheme-handler/idcbrowser") + + elif platform.system() == "Windows": + + # Get the directory of the current Python executable + python_dir = os.path.dirname(sys.executable) + + # Construct the path to PythonSlicer.exe in the same directory + python_path = os.path.join(python_dir, "PythonSlicer.exe") + + current_dir = os.path.dirname(os.path.realpath(__file__)) + python_script_path = os.path.join(current_dir,'Resources', 'resolver.py') + + # Register IDC Browser URL protocol in Windows Registry + import winreg as reg + + try: + reg.CreateKey(reg.HKEY_CURRENT_USER, r"Software\Classes\idcbrowser") + with reg.OpenKey(reg.HKEY_CURRENT_USER, r"Software\Classes\idcbrowser", 0, reg.KEY_WRITE) as key: + reg.SetValue(key, None, reg.REG_SZ, "URL:IDC Browser Protocol") + reg.SetValueEx(key, "URL Protocol", 0, reg.REG_SZ, "") + + reg.CreateKey(reg.HKEY_CURRENT_USER, r"Software\Classes\idcbrowser\shell\open\command") + with reg.OpenKey(reg.HKEY_CURRENT_USER, r"Software\Classes\idcbrowser\shell\open\command", 0, reg.KEY_WRITE) as key: + reg.SetValue(key, None, reg.REG_SZ, f'"{python_path}" "{python_script_path}" "%1"') + + print("IDC Browser URL protocol has been registered on Windows.") + except Exception as e: + print(f"Failed to register IDC Browser URL protocol on Windows: {e}") else: print("IDC Browser URL protocol registration is not supported on this operating system.") From 4b94d14f63828434c48b47af37c2dc8c651e44ab Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi <115020590+vkt1414@users.noreply.github.com> Date: Thu, 18 Jul 2024 09:42:24 -0400 Subject: [PATCH 05/14] enh: add macos registration for idcbrowser:// protocol --- IDCBrowser/IDCHandlerModule.py | 112 ++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index 2c543f8..c4944c3 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -17,6 +17,7 @@ import slicer import ctk from idc_index import index +import sys class IDCRequestHandler(BaseRequestHandler): @@ -222,7 +223,114 @@ def registerCustomProtocol(self): print("IDC Browser URL protocol has been registered on Windows.") except Exception as e: - print(f"Failed to register IDC Browser URL protocol on Windows: {e}") + print(f"Failed to register IDC Browser URL protocol on Windows: {e}") + + elif platform.system() == "Darwin": + slicer_exec_dir = os.path.dirname(sys.executable) + + # Construct the path to PythonSlicer.exe in the same directory + python_path = os.path.join(slicer_exec_dir, "bin", "PythonSlicer") + + current_dir = os.path.dirname(os.path.realpath(__file__)) + python_script_path = os.path.join(current_dir,'Resources', 'resolver.py') + + def check_macos_slicer_protocol_registration(): + plist_path = os.path.expanduser("/Applications/slicer-app.app/Contents/Info.plist") + return os.path.exists(plist_path) + + if check_macos_slicer_protocol_registration(): + print("Slicer URL protocol is already registered.") + return + + # Create the AppleScript + applescript_path = os.path.expanduser("~/slicer.applescript") + with open(applescript_path, "w") as applescript_file: + applescript_file.write(f""" + on open location this_URL + do shell script "{python_path} {python_script_path} " & quoted form of this_URL + end open location + """) + + + # Compile the AppleScript into an app + os.system(f"osacompile -o /Applications/slicer-app.app {applescript_path}") + + + # Create or modify the plist file + plist_content = """ + + + + + CFBundleAllowMixedLocalizations + + CFBundleDevelopmentRegion + en + CFBundleExecutable + applet + CFBundleIconFile + applet + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + slicer-app + CFBundlePackageType + APPL + CFBundleSignature + aplt + LSMinimumSystemVersionByArchitecture + + x86_64 + 10.6 + + LSRequiresCarbon + + NSAppleEventsUsageDescription + This script needs to control other applications to run. + NSAppleMusicUsageDescription + This script needs access to your music to run. + NSCalendarsUsageDescription + This script needs access to your calendars to run. + NSCameraUsageDescription + This script needs access to your camera to run. + NSContactsUsageDescription + This script needs access to your contacts to run. + NSHomeKitUsageDescription + This script needs access to your HomeKit Home to run. + NSMicrophoneUsageDescription + This script needs access to your microphone to run. + NSPhotoLibraryUsageDescription + This script needs access to your photos to run. + NSRemindersUsageDescription + This script needs access to your reminders to run. + NSSiriUsageDescription + This script needs access to Siri to run. + NSSystemAdministrationUsageDescription + This script needs access to administer this system to run. + OSAAppletShowStartupScreen + + CFBundleIdentifier + slicer.protocol.registration + CFBundleURLTypes + + + CFBundleURLName + idcbrowser + CFBundleURLSchemes + + idcbrowser + + + + + + """ + + plist_path = os.path.expanduser("/Applications/slicer-app.app/Contents/Info.plist") + with open(plist_path, "w") as plist_file: + plist_file.write(plist_content) + + print("Slicer URL protocol registered successfully.") + else: print("IDC Browser URL protocol registration is not supported on this operating system.") - From 8776a2350665d37202aa93cb8913e22954807ba6 Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi <115020590+vkt1414@users.noreply.github.com> Date: Thu, 18 Jul 2024 09:56:29 -0400 Subject: [PATCH 06/14] bug(fix): fix the python path --- IDCBrowser/IDCHandlerModule.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index c4944c3..dd033cf 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -226,10 +226,10 @@ def registerCustomProtocol(self): print(f"Failed to register IDC Browser URL protocol on Windows: {e}") elif platform.system() == "Darwin": - slicer_exec_dir = os.path.dirname(sys.executable) - - # Construct the path to PythonSlicer.exe in the same directory - python_path = os.path.join(slicer_exec_dir, "bin", "PythonSlicer") + grandparent_dir = os.path.dirname(os.path.dirname(slicer_dir)) + + # Now, you can construct the path to PythonSlicer + python_path = os.path.join(grandparent_dir, "bin", "PythonSlicer") current_dir = os.path.dirname(os.path.realpath(__file__)) python_script_path = os.path.join(current_dir,'Resources', 'resolver.py') From aa4261b1031b865feaebfd762cbba775d9575b69 Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi <115020590+vkt1414@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:15:10 -0400 Subject: [PATCH 07/14] bug(fix): add slicer executable's path --- IDCBrowser/IDCHandlerModule.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index dd033cf..c946897 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -226,7 +226,8 @@ def registerCustomProtocol(self): print(f"Failed to register IDC Browser URL protocol on Windows: {e}") elif platform.system() == "Darwin": - grandparent_dir = os.path.dirname(os.path.dirname(slicer_dir)) + slicer_exec_dir = os.path.dirname(sys.executable) + grandparent_dir = os.path.dirname(os.path.dirname(slicer_exec_dir)) # Now, you can construct the path to PythonSlicer python_path = os.path.join(grandparent_dir, "bin", "PythonSlicer") From 85fb6a4e5aa974ae6970e8e8916f26bfa921d7b0 Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi <115020590+vkt1414@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:36:39 -0400 Subject: [PATCH 08/14] bug(fix): fix python path --- IDCBrowser/IDCHandlerModule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index c946897..6902c81 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -227,10 +227,10 @@ def registerCustomProtocol(self): elif platform.system() == "Darwin": slicer_exec_dir = os.path.dirname(sys.executable) - grandparent_dir = os.path.dirname(os.path.dirname(slicer_exec_dir)) + parent_dir = os.path.dirname(slicer_exec_dir) # Now, you can construct the path to PythonSlicer - python_path = os.path.join(grandparent_dir, "bin", "PythonSlicer") + python_path = os.path.join(parent_dir, "bin", "PythonSlicer") current_dir = os.path.dirname(os.path.realpath(__file__)) python_script_path = os.path.join(current_dir,'Resources', 'resolver.py') From c24b19691fe5d9a971e6cc2bfa34631fd5488518 Mon Sep 17 00:00:00 2001 From: vkt1414 Date: Wed, 24 Jul 2024 12:28:29 -0400 Subject: [PATCH 09/14] enh: fix the paths using shlex to make the paths work on shell --- IDCBrowser/IDCHandlerModule.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index 6902c81..bb13164 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -18,6 +18,7 @@ import ctk from idc_index import index import sys +import shlex class IDCRequestHandler(BaseRequestHandler): @@ -230,10 +231,10 @@ def registerCustomProtocol(self): parent_dir = os.path.dirname(slicer_exec_dir) # Now, you can construct the path to PythonSlicer - python_path = os.path.join(parent_dir, "bin", "PythonSlicer") + python_path = shlex.quote(os.path.join(parent_dir, "bin", "PythonSlicer")) current_dir = os.path.dirname(os.path.realpath(__file__)) - python_script_path = os.path.join(current_dir,'Resources', 'resolver.py') + python_script_path = shlex.quote(os.path.join(current_dir,'Resources', 'resolver.py')) def check_macos_slicer_protocol_registration(): plist_path = os.path.expanduser("/Applications/slicer-app.app/Contents/Info.plist") From b30252bf0faced6e30102ac7b238f6c543475526 Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi Date: Sun, 28 Jul 2024 03:19:04 +0000 Subject: [PATCH 10/14] enh: use PythonSlicer instead of users python in ubuntu as well use shell quote to be resilient to spaces in paths add acknowledgement text reformat code using black code formatter --- IDCBrowser/IDCHandlerModule.py | 180 +++++++++++++++++++++++---------- 1 file changed, 126 insertions(+), 54 deletions(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index bb13164..5928dec 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -4,7 +4,10 @@ from slicer.ScriptedLoadableModule import * from WebServer import WebServerLogic from typing import Optional -from WebServerLib.BaseRequestHandler import BaseRequestHandler, BaseRequestLoggingFunction +from WebServerLib.BaseRequestHandler import ( + BaseRequestHandler, + BaseRequestLoggingFunction, +) import os import platform @@ -20,6 +23,7 @@ import sys import shlex + class IDCRequestHandler(BaseRequestHandler): def __init__(self, logMessage: Optional[BaseRequestLoggingFunction] = None): @@ -29,11 +33,13 @@ def __init__(self, logMessage: Optional[BaseRequestLoggingFunction] = None): def canHandleRequest(self, uri: bytes, **_kwargs) -> float: parsedURL = urllib.parse.urlparse(uri) - if (parsedURL.path.startswith(b"/idc")): + if parsedURL.path.startswith(b"/idc"): return 0.5 return 0.0 - def handleRequest(self, method: str, uri: bytes, requestBody: bytes, **_kwargs) -> tuple[bytes, bytes]: + def handleRequest( + self, method: str, uri: bytes, requestBody: bytes, **_kwargs + ) -> tuple[bytes, bytes]: parsedURL = urllib.parse.urlparse(uri) splitPath = parsedURL.path.split(b"/") @@ -45,8 +51,10 @@ def handleRequest(self, method: str, uri: bytes, requestBody: bytes, **_kwargs) return self.handleDownload(series_uids) elif splitPath[2] == b"download" and splitPath[3] == b"studyInstanceUID": study_uids = splitPath[4].decode().split(",") - filtered_df = self.index_df[self.index_df['StudyInstanceUID'].isin(study_uids)] - series_uids_from_study_uid = filtered_df['SeriesInstanceUID'].tolist() + filtered_df = self.index_df[ + self.index_df["StudyInstanceUID"].isin(study_uids) + ] + series_uids_from_study_uid = filtered_df["SeriesInstanceUID"].tolist() return self.handleDownload(series_uids_from_study_uid) else: return b"text/plain", b"Unhandled IDC request path" @@ -63,14 +71,18 @@ def handleCollections(self) -> tuple[bytes, bytes]: contentType = b"text/plain" if self.logMessage: self.logMessage(responseBody.decode()) - return contentType, responseBody def handleDownload(self, uids: list[str]) -> tuple[bytes, bytes]: destFolderPath = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() try: - self.client.download_from_selection(seriesInstanceUID=uids, downloadDir=destFolderPath, dirTemplate="%SeriesInstanceUID") + self.client.download_from_selection( + seriesInstanceUID=uids, + downloadDir=destFolderPath, + dirTemplate="%SeriesInstanceUID", + use_s5cmd_sync=True, + ) indexer = ctk.ctkDICOMIndexer() for uid in uids: download_folder_path = os.path.join(destFolderPath, uid) @@ -83,16 +95,18 @@ def handleDownload(self, uids: list[str]) -> tuple[bytes, bytes]: volume = plugin.load(loadables[0]) logging.debug("Loaded volume: " + volume.GetName()) else: - raise Exception("Unable to load DICOM content. Please retry from DICOM Browser!") - - responseBody = f"Downloaded and indexed UID(s): {', '.join(uids)}".encode() + raise Exception( + "Unable to load DICOM content. Please retry from DICOM Browser!" + ) + responseBody = ( + f"Downloaded and indexed UID(s): {', '.join(uids)}".encode() + ) contentType = b"text/plain" except Exception as e: responseBody = f"Error downloading or indexing UID(s): {e}".encode() contentType = b"text/plain" if self.logMessage: self.logMessage(responseBody.decode()) - return contentType, responseBody @@ -101,9 +115,15 @@ def __init__(self, parent): ScriptedLoadableModule.__init__(self, parent) self.parent.title = "IDC Handler Module" self.parent.categories = ["Examples"] - self.parent.contributors = ["Your Name (Your Organization)"] + self.parent.contributors = [ + "Vamsi Thiriveedhi(ImagingDataCommons), Steve Pieper (Isomics, Inc.)" + ] self.parent.helpText = """This module registers an IDCRequestHandler to handle IDC-related requests.""" - self.parent.acknowledgementText = """This module was developed by Your Name, Your Organization.""" + self.parent.acknowledgementText = """This was a project born during PW 40 when @pieper once mentioned the idea of using Slicer just the way we use zoom. +This post (https://discourse.slicer.org/t/how-to-load-nifti-file-from-web-browser-link/18664/5) showed that it was indeed possible, and the current implementation +is inspired from it, and the slicerio package, which was originally developed by @lassoan and team. +see https://github.com/ImagingDataCommons/SlicerIDCBrowser/pull/43 for more info. +""" slicer.app.connect("startupCompleted()", self.onStartupCompleted) @@ -115,22 +135,28 @@ def onStartupCompleted(self): logic.stop() except NameError: pass - logMessage = WebServerLogic.defaultLogMessage requestHandlers = [IDCRequestHandler()] - logic = WebServerLogic(port=PORT, logMessage=logMessage, enableSlicer=True, enableStaticPages=False, enableDICOM=True, requestHandlers=requestHandlers) + logic = WebServerLogic( + port=PORT, + logMessage=logMessage, + enableSlicer=True, + enableStaticPages=False, + enableDICOM=True, + requestHandlers=requestHandlers, + ) logic.start() print("IDC Request Handler has been registered and server started.") - + self.writeResolverScript() self.registerCustomProtocol() def writeResolverScript(self): current_dir = os.path.dirname(os.path.realpath(__file__)) - resolver_script_path = os.path.join(current_dir,'Resources', 'resolver.py') + resolver_script_path = os.path.join(current_dir, "Resources", "resolver.py") - resolver_script_content = '''import sys + resolver_script_content = """import sys import urllib.parse import requests import webbrowser @@ -167,98 +193,142 @@ def resolve_url(url): # Resolve the URL resolve_url(url) -''' +""" - with open(resolver_script_path, 'w') as f: + with open(resolver_script_path, "w") as f: f.write(resolver_script_content) print(f"Resolver script written to {resolver_script_path}") def registerCustomProtocol(self): if platform.system() == "Linux": # Check if the protocol is already registered - if os.path.exists(os.path.expanduser("~/.local/share/applications/idcbrowser.desktop")): + + if os.path.exists( + os.path.expanduser("~/.local/share/applications/idcbrowser.desktop") + ): print("IDC Browser URL protocol is already registered.") return - # Get the current directory + current_dir = os.path.dirname(os.path.realpath(__file__)) - python_script_path = os.path.join(current_dir, 'resolver.py') + python_script_path = shlex.quote( + os.path.join(current_dir, "Resources", "resolver.py") + ) + + python_dir = slicer.app.slicerHome + normalized_python_dir = os.path.normpath(python_dir) + + # Construct the path to PythonSlicer.exe in the same directory + python_path = shlex.quote( + os.path.join(normalized_python_dir, "bin", "PythonSlicer") + ) # Register IDC Browser URL protocol - with open(os.path.expanduser("~/.local/share/applications/idcbrowser.desktop"), "w") as f: - f.write(f"""[Desktop Entry] + + with open( + os.path.expanduser("~/.local/share/applications/idcbrowser.desktop"), + "w", + ) as f: + f.write( + f"""[Desktop Entry] Name=IDC Browser -Exec=python3 {python_script_path} %u +Exec={python_path} {python_script_path} %u Type=Application Terminal=false MimeType=x-scheme-handler/idcbrowser; -""") - +""" + ) # Update MIME database + os.system("update-desktop-database ~/.local/share/applications/") os.system("xdg-mime default idcbrowser.desktop x-scheme-handler/idcbrowser") - elif platform.system() == "Windows": - + # Get the directory of the current Python executable + python_dir = os.path.dirname(sys.executable) # Construct the path to PythonSlicer.exe in the same directory - python_path = os.path.join(python_dir, "PythonSlicer.exe") + + python_path = shlex.quote(os.path.join(python_dir, "PythonSlicer.exe")) current_dir = os.path.dirname(os.path.realpath(__file__)) - python_script_path = os.path.join(current_dir,'Resources', 'resolver.py') + python_script_path = shlex.quote( + os.path.join(current_dir, "Resources", "resolver.py") + ) # Register IDC Browser URL protocol in Windows Registry + import winreg as reg try: reg.CreateKey(reg.HKEY_CURRENT_USER, r"Software\Classes\idcbrowser") - with reg.OpenKey(reg.HKEY_CURRENT_USER, r"Software\Classes\idcbrowser", 0, reg.KEY_WRITE) as key: + with reg.OpenKey( + reg.HKEY_CURRENT_USER, + r"Software\Classes\idcbrowser", + 0, + reg.KEY_WRITE, + ) as key: reg.SetValue(key, None, reg.REG_SZ, "URL:IDC Browser Protocol") reg.SetValueEx(key, "URL Protocol", 0, reg.REG_SZ, "") - - reg.CreateKey(reg.HKEY_CURRENT_USER, r"Software\Classes\idcbrowser\shell\open\command") - with reg.OpenKey(reg.HKEY_CURRENT_USER, r"Software\Classes\idcbrowser\shell\open\command", 0, reg.KEY_WRITE) as key: - reg.SetValue(key, None, reg.REG_SZ, f'"{python_path}" "{python_script_path}" "%1"') - + reg.CreateKey( + reg.HKEY_CURRENT_USER, + r"Software\Classes\idcbrowser\shell\open\command", + ) + with reg.OpenKey( + reg.HKEY_CURRENT_USER, + r"Software\Classes\idcbrowser\shell\open\command", + 0, + reg.KEY_WRITE, + ) as key: + reg.SetValue( + key, + None, + reg.REG_SZ, + f'"{python_path}" "{python_script_path}" "%1"', + ) print("IDC Browser URL protocol has been registered on Windows.") except Exception as e: - print(f"Failed to register IDC Browser URL protocol on Windows: {e}") - - elif platform.system() == "Darwin": + print(f"Failed to register IDC Browser URL protocol on Windows: {e}") + elif platform.system() == "Darwin": slicer_exec_dir = os.path.dirname(sys.executable) parent_dir = os.path.dirname(slicer_exec_dir) - + # Now, you can construct the path to PythonSlicer + python_path = shlex.quote(os.path.join(parent_dir, "bin", "PythonSlicer")) current_dir = os.path.dirname(os.path.realpath(__file__)) - python_script_path = shlex.quote(os.path.join(current_dir,'Resources', 'resolver.py')) + python_script_path = shlex.quote( + os.path.join(current_dir, "Resources", "resolver.py") + ) def check_macos_slicer_protocol_registration(): - plist_path = os.path.expanduser("/Applications/slicer-app.app/Contents/Info.plist") + plist_path = os.path.expanduser( + "/Applications/slicer-app.app/Contents/Info.plist" + ) return os.path.exists(plist_path) if check_macos_slicer_protocol_registration(): print("Slicer URL protocol is already registered.") return - # Create the AppleScript + applescript_path = os.path.expanduser("~/slicer.applescript") with open(applescript_path, "w") as applescript_file: - applescript_file.write(f""" + applescript_file.write( + f""" on open location this_URL do shell script "{python_path} {python_script_path} " & quoted form of this_URL end open location - """) - - + """ + ) # Compile the AppleScript into an app - os.system(f"osacompile -o /Applications/slicer-app.app {applescript_path}") + os.system(f"osacompile -o /Applications/slicer-app.app {applescript_path}") # Create or modify the plist file + plist_content = """ @@ -328,11 +398,13 @@ def check_macos_slicer_protocol_registration(): """ - plist_path = os.path.expanduser("/Applications/slicer-app.app/Contents/Info.plist") + plist_path = os.path.expanduser( + "/Applications/slicer-app.app/Contents/Info.plist" + ) with open(plist_path, "w") as plist_file: plist_file.write(plist_content) - print("Slicer URL protocol registered successfully.") - else: - print("IDC Browser URL protocol registration is not supported on this operating system.") + print( + "IDC Browser URL protocol registration is not supported on this operating system." + ) From cc5658d7287129a8e3e82de34275134e1746fc5d Mon Sep 17 00:00:00 2001 From: vkt1414 Date: Sat, 27 Jul 2024 23:41:00 -0400 Subject: [PATCH 11/14] bug(fix): remove shell quote for windows paths --- IDCBrowser/IDCHandlerModule.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index 5928dec..7c0aed6 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -250,12 +250,10 @@ def registerCustomProtocol(self): # Construct the path to PythonSlicer.exe in the same directory - python_path = shlex.quote(os.path.join(python_dir, "PythonSlicer.exe")) + python_path = os.path.join(python_dir, "PythonSlicer.exe") current_dir = os.path.dirname(os.path.realpath(__file__)) - python_script_path = shlex.quote( - os.path.join(current_dir, "Resources", "resolver.py") - ) + python_script_path = os.path.join(current_dir, "Resources", "resolver.py") # Register IDC Browser URL protocol in Windows Registry From d60704191527805875fe3ce05510b1f9269bb560 Mon Sep 17 00:00:00 2001 From: Vamsi Thiriveedhi <115020590+vkt1414@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:33:25 -0400 Subject: [PATCH 12/14] bug: remove use s5cmd sync --- IDCBrowser/IDCHandlerModule.py | 1 - 1 file changed, 1 deletion(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index 7c0aed6..7b97349 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -81,7 +81,6 @@ def handleDownload(self, uids: list[str]) -> tuple[bytes, bytes]: seriesInstanceUID=uids, downloadDir=destFolderPath, dirTemplate="%SeriesInstanceUID", - use_s5cmd_sync=True, ) indexer = ctk.ctkDICOMIndexer() for uid in uids: From dd81624f3ae72eca4b19ecdf31df8cdf9e967056 Mon Sep 17 00:00:00 2001 From: vkt1414 Date: Fri, 16 Aug 2024 11:44:34 -0400 Subject: [PATCH 13/14] enh: use user homedir instead of root, remove writing resolver.py --- IDCBrowser/IDCHandlerModule.py | 53 ++-------------------------------- 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index 7b97349..30bdb03 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -148,55 +148,8 @@ def onStartupCompleted(self): logic.start() print("IDC Request Handler has been registered and server started.") - self.writeResolverScript() self.registerCustomProtocol() - def writeResolverScript(self): - current_dir = os.path.dirname(os.path.realpath(__file__)) - resolver_script_path = os.path.join(current_dir, "Resources", "resolver.py") - - resolver_script_content = """import sys -import urllib.parse -import requests -import webbrowser - -def resolve_url(url): - # Parse the URL - parsed_url = urllib.parse.urlparse(url) - - # Remove the scheme (idcbrowser://) from the URL and split the path - path_parts = parsed_url.netloc.split('/') + parsed_url.path.split('/')[1:] - - # Check the first part of the path to determine the endpoint - if path_parts[0] == 'collections': - new_url = "http://localhost:2042/idc/collections" - # Open the new URL in a web browser - webbrowser.open(new_url) - elif path_parts[0] == 'series': - new_url = f"http://localhost:2042/idc/download/seriesInstanceUID/{path_parts[1]}" - elif path_parts[0] == 'studies': - new_url = f"http://localhost:2042/idc/download/studyInstanceUID/{path_parts[1]}" - else: - print(f"Unhandled path: {path_parts[0]}") - return - - # Make the request to the new URL - response = requests.get(new_url) - - # Print the response - print(response.text) - -if __name__ == "__main__": - # The URL is passed as the first argument - url = sys.argv[1] - - # Resolve the URL - resolve_url(url) -""" - - with open(resolver_script_path, "w") as f: - f.write(resolver_script_content) - print(f"Resolver script written to {resolver_script_path}") def registerCustomProtocol(self): if platform.system() == "Linux": @@ -302,7 +255,7 @@ def registerCustomProtocol(self): def check_macos_slicer_protocol_registration(): plist_path = os.path.expanduser( - "/Applications/slicer-app.app/Contents/Info.plist" + "~/Applications/slicer-app.app/Contents/Info.plist" ) return os.path.exists(plist_path) @@ -322,7 +275,7 @@ def check_macos_slicer_protocol_registration(): ) # Compile the AppleScript into an app - os.system(f"osacompile -o /Applications/slicer-app.app {applescript_path}") + os.system(f"osacompile -o ~/Applications/slicer-app.app {applescript_path}") # Create or modify the plist file @@ -396,7 +349,7 @@ def check_macos_slicer_protocol_registration(): """ plist_path = os.path.expanduser( - "/Applications/slicer-app.app/Contents/Info.plist" + "~/Applications/slicer-app.app/Contents/Info.plist" ) with open(plist_path, "w") as plist_file: plist_file.write(plist_content) From 3fbf9dca19d172e5d81afb510f27ff6f6b39a9ae Mon Sep 17 00:00:00 2001 From: vkt1414 Date: Fri, 16 Aug 2024 11:54:10 -0400 Subject: [PATCH 14/14] enh: use slicer temporary path to compile the app --- IDCBrowser/IDCHandlerModule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IDCBrowser/IDCHandlerModule.py b/IDCBrowser/IDCHandlerModule.py index 30bdb03..5990faa 100644 --- a/IDCBrowser/IDCHandlerModule.py +++ b/IDCBrowser/IDCHandlerModule.py @@ -263,8 +263,8 @@ def check_macos_slicer_protocol_registration(): print("Slicer URL protocol is already registered.") return # Create the AppleScript - - applescript_path = os.path.expanduser("~/slicer.applescript") + applescript_path = slicer.app.temporaryPath + "/slicer.applescript" + #applescript_path = os.path.expanduser("~/slicer.applescript") with open(applescript_path, "w") as applescript_file: applescript_file.write( f"""