11import atexit
22import json
33import os
4+ import platform
45import queue
56import select
7+ import subprocess
68import sys
79import threading
810import traceback
911from pathlib import Path
1012import asyncio
1113
1214from threading import Thread
15+ from time import sleep
16+
17+ import requests
1318from packaging .version import Version
1419
1520import pymobiledevice3 .usbmux as usbmux
1621from pymobiledevice3 .exceptions import *
22+ from pymobiledevice3 .remote .common import TunnelProtocol
1723from pymobiledevice3 .services .installation_proxy import InstallationProxyService
1824from pymobiledevice3 .services .mobile_image_mounter import auto_mount
1925from pymobiledevice3 .tcp_forwarder import LockdownTcpForwarder , UsbmuxTcpForwarder
20- from pymobiledevice3 .tunneld .api import get_tunneld_device_by_udid
26+ from pymobiledevice3 .tunneld .api import get_tunneld_device_by_udid , TUNNELD_DEFAULT_ADDRESS
27+ from pymobiledevice3 .tunneld .server import TunneldRunner
2128from pymobiledevice3 .usbmux import *
2229from pymobiledevice3 .lockdown import create_using_usbmux
2330import plistlib
@@ -200,6 +207,60 @@ def auto_mount_image(id, lockdown, writer):
200207 write_dispatcher .write_reply (writer , reply )
201208
202209
210+ def start_tunneld ():
211+ if platform .system () == "Darwin" :
212+ print ("Starting tunneld as process" )
213+ python_executable = sys .executable
214+ start_script = f'do shell script "sudo { python_executable } -m pymobiledevice3 remote tunneld" with administrator privileges'
215+ print ("Running \" " + start_script + "\" " )
216+ process = subprocess .Popen (['osascript' , '-e' , start_script ], stdout = None , stderr = None )
217+ print (f"Started tunneld process with pid { process .pid } " )
218+ else :
219+ print ("Starting tunneld as thread" )
220+ def _worker ():
221+ TunneldRunner .create (* TUNNELD_DEFAULT_ADDRESS , protocol = TunnelProtocol .DEFAULT )
222+
223+ webserver_thread = threading .Thread (target = _worker , daemon = True , name = "Python-Tunneld" )
224+ webserver_thread .start ()
225+
226+ for i in range (60 ):
227+ if is_tunneld_running ():
228+ print ("tunneld successfully started" )
229+ return
230+ sleep (1 )
231+ raise RuntimeError ("Failed launching tunneld service" )
232+
233+ def is_tunneld_running ():
234+ try :
235+ response = requests .get (f"http://{ TUNNELD_DEFAULT_ADDRESS [0 ]} :{ TUNNELD_DEFAULT_ADDRESS [1 ]} /hello" )
236+ if response .status_code != 200 :
237+ raise RuntimeError ()
238+
239+ data = response .json ()
240+ if data .get ("message" ) != "Hello, I'm alive" :
241+ raise RuntimeError ()
242+ # Tunneld seems running happily
243+ return True
244+ except Exception :
245+ return False
246+
247+ def shutdown_tunneld ():
248+ if is_tunneld_running ():
249+ try :
250+ response = requests .get (f"http://{ TUNNELD_DEFAULT_ADDRESS [0 ]} :{ TUNNELD_DEFAULT_ADDRESS [1 ]} /shutdown" )
251+ if response .status_code != 200 :
252+ raise RuntimeError ("Status code was " + str (response .status_code ))
253+ print ("tunneld shutdown request successful" )
254+ except Exception as e :
255+ print ("Failed tunneld teardown" , e )
256+
257+ def ensure_tunneld_running ():
258+ if not is_tunneld_running ():
259+ start_tunneld ()
260+ print ("tunneld is not running - starting" )
261+ else :
262+ print ("tunneld is already running" )
263+
203264def debugserver_connect (id , lockdown , port , writer ):
204265 try :
205266 discovery_service = get_tunneld_device_by_udid (lockdown .udid )
@@ -292,7 +353,7 @@ def handle_command(command, writer):
292353
293354 command_type = res ['command' ]
294355 if command_type == "exit" :
295- print ("Exciting by request" )
356+ print ("Exiting by request" )
296357 atexit ._run_exitfuncs ()
297358 os ._exit (0 )
298359 elif command_type == "list_devices" :
@@ -317,6 +378,16 @@ def handle_command(command, writer):
317378 elif command_type == "usbmux_forwarder_close" :
318379 usbmux_forward_close (id , res ['local_port' ], writer )
319380 return
381+ elif command_type == "ensure_tunneld_running" :
382+ ensure_tunneld_running ()
383+ reply = {"id" : id , "state" : "completed" }
384+ write_dispatcher .write_reply (writer , reply )
385+ return
386+ elif command_type == "is_tunneld_running" :
387+ res = is_tunneld_running ()
388+ reply = {"id" : id , "state" : "completed" , "result" : res }
389+ write_dispatcher .write_reply (writer , reply )
390+ return
320391
321392 # Now come the device targetted functions
322393 device_id = res ['device_id' ]
@@ -355,6 +426,7 @@ def main():
355426 print (f"Written port { port } to file { path } " )
356427
357428 atexit .register (os .remove , path )
429+ atexit .register (shutdown_tunneld )
358430 print (f"Start listening on port { port } " )
359431 while True :
360432 client_socket , client_address = server .accept ()
0 commit comments