diff --git a/.gitignore b/.gitignore index 7a605bc..5133521 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .vscode/ __pycache__/ + +.DS_Store \ No newline at end of file diff --git a/HNroute.txt b/HNroute.txt index 59d3883..e69de29 100644 --- a/HNroute.txt +++ b/HNroute.txt @@ -1 +0,0 @@ -{"lng":"120.7335575167566","lat":"30.52802386594508"},{"lng":"120.7335664998117","lat":"30.527949984828833"},{"lng":"120.73357997439435","lat":"30.527887769107984"},{"lng":"120.73361590661474","lat":"30.527825553346897"},{"lng":"120.7336653134178","lat":"30.527771114522935"},{"lng":"120.73372370327594","lat":"30.527720564158813"},{"lng":"120.73379107618919","lat":"30.527689456229226"},{"lng":"120.73387641521265","lat":"30.527662236782586"},{"lng":"120.73397073729119","lat":"30.52765057130309"},{"lng":"120.73407404242484","lat":"30.527646682809618"},{"lng":"120.73416387297584","lat":"30.527677790753035"},{"lng":"120.73428514421968","lat":"30.52773611811983"},{"lng":"120.73436150018803","lat":"30.52781388788721"},{"lng":"120.73441539851864","lat":"30.527922765455916"},{"lng":"120.73444234768392","lat":"30.52803941985705"},{"lng":"120.73445133073903","lat":"30.528156074116726"},{"lng":"120.73445582226658","lat":"30.5283038359759"},{"lng":"120.73446031379413","lat":"30.528420489914918"},{"lng":"120.73446031379413","lat":"30.52852158988096"},{"lng":"120.73446480532168","lat":"30.528618801286576"},{"lng":"120.73446480532168","lat":"30.528719901044198"},{"lng":"120.73445582226658","lat":"30.52881322380307"},{"lng":"120.73442887310128","lat":"30.528902658028688"},{"lng":"120.73439294088088","lat":"30.52898820373192"},{"lng":"120.73434353407784","lat":"30.529034864992525"},{"lng":"120.73427616116459","lat":"30.529081526230495"},{"lng":"120.73420429672379","lat":"30.52912429901208"},{"lng":"120.73414590686563","lat":"30.529143741179244"},{"lng":"120.73408302547993","lat":"30.52915929491014"},{"lng":"120.73399319492894","lat":"30.52915540647765"},{"lng":"120.73390785590549","lat":"30.52915540647765"},{"lng":"120.73383599146469","lat":"30.529135964312847"},{"lng":"120.733773110079","lat":"30.529108745275526"},{"lng":"120.73372370327594","lat":"30.529065972487018"},{"lng":"120.73365633036269","lat":"30.529003757487974"},{"lng":"120.73361590661474","lat":"30.528925988682573"},{"lng":"120.7335754828668","lat":"30.528855996703964"},{"lng":"120.7335664998117","lat":"30.528782116226836"},{"lng":"120.7335575167566","lat":"30.528719901044198"},{"lng":"120.7335575167566","lat":"30.52863435510235"},{"lng":"120.7335575167566","lat":"30.528541032169947"},{"lng":"120.7335575167566","lat":"30.528451597608097"},{"lng":"120.7335485337015","lat":"30.528350497568482"},{"lng":"120.73355302522906","lat":"30.528261062829475"},{"lng":"120.7335575167566","lat":"30.52818329342363"},{"lng":"120.73356200828415","lat":"30.528128854802127"} \ No newline at end of file diff --git a/README.md b/README.md index fae340b..c95e2f1 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# iOSRealRun-cli-17 +# iOSLocationSimulator-cli-18 ## 用法简介 ### 前置条件 1. 系统是 `Windows` 或 `MacOS` -2. iPhone 或 iPad 系统版本大于等于 17 -3. Windows 需要安装 iTunes -4. 已安装 `Python3` 和 `pip3` +2. iPhone 或 iPad 系统版本 >= 17(支持 iOS 17 ~ iOS 26+) +3. Windows 需要安装 iTunes(从 Microsoft Store 安装) +4. 已安装 `Python 3.10+` 和 `pip3` 5. **重要**: 只能有一台 iPhone 或 iPad 连接到电脑,否则会出问题 ### 步骤 @@ -18,7 +18,7 @@ pip3 install -r requirements.txt ``` 如果 `pip3` 无法安装,请使用 `pip` 替代 - 如果提示没有需要的版本,请尝试不适用国内源 + 如果提示没有需要的版本,请尝试不使用国内源 3. 修改配置和路线文件 (见 [这里](https://github.com/iOSRealRun/iOSRealRun-cli/blob/main/README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95) 的 4、5、7 步) 4. 将设备连接到电脑,解锁,如果请求信任的提示框,请点击信任 5. Windows **以管理员身份** 打开终端(cmd 或 PowerShell),先进入项目目录,然后执行以下命令 @@ -29,8 +29,27 @@ ```shell sudo python3 main.py ``` - > 需要 管理员 或 root 权限是因为需要创建 tun 设备 + > 需要 管理员 或 root 权限是因为需要创建 TUN 设备 6. 按照提示操作,如果一直说没有设备连接,Windows请确保 iTunes 已安装(可能需要打开),重新运行程序,在第3步时请确保设备已连接,解锁并信任 7. 结束请务必使用 `Ctrl + C` 终止程序,否则无法恢复定位 8. 如果定位未恢复,可以重启手机解决 + +### iOS 26 适配说明 + +此版本已适配 iOS 26(及 iOS 18.2+)的连接方式变更: +- 隧道协议使用 **TCP**(iOS 18.2+ 已移除 QUIC 协议支持) +- 依赖库升级至 pymobiledevice3 >= 9.12.0 +- DVT 连接使用新的 `DvtProvider` / `LocationSimulation` 异步 API +- 完整复现原有模拟定位功能(坐标系转换、路径插值、随机扰动等) + + +### 声明 + + 1. 此软件旨在帮助开发者等专业人士进行软件开发,游戏开发,功能调试,应用兼容性测试等位置信息相关功能的开发调试工具。 + + 2. 用户(下称“您”)需要确保不得通过本软件以任何方式进行非法、欺诈、侵犯第三方权益、违反其他应用服务条款的行为,包括但不限于用于办公打卡、签到、网约车、校园跑、配送服务相关的行为。我们保留向监管部门举报您违法违规行为的权利。 + + 3. 您知悉并同意本软件仅限您用于合法的软件开发、功能调试、应用兼容性测试等专业目的。并且未经我们同意,您不得基于本软件进一步对外提供服务。 + + 4. 受限于技术能力,我们暂时无法主动识别并对已知的、易被用于非法、欺诈、侵犯第三方权益或违反其他应用服务条款的高风险应用(APP),采取技术措施进行功能作用屏蔽,您不得尝试绕过或破坏我们的屏蔽措施,否则将承担本协议第2条所述的严重违约责任。 \ No newline at end of file diff --git a/driver/connect.py b/driver/connect.py index b4be45d..a7f59a5 100644 --- a/driver/connect.py +++ b/driver/connect.py @@ -2,54 +2,58 @@ import multiprocessing from pymobiledevice3.lockdown import create_using_usbmux, LockdownClient - -from pymobiledevice3.cli.remote import install_driver_if_required -from pymobiledevice3.cli.remote import select_device, RemoteServiceDiscoveryService -from pymobiledevice3.cli.remote import start_tunnel -from pymobiledevice3.cli.remote import verify_tunnel_imports - +from pymobiledevice3.remote.tunnel_service import CoreDeviceTunnelProxy from pymobiledevice3.services.amfi import AmfiService - from pymobiledevice3.exceptions import NoDeviceConnectedError -def get_usbmux_lockdownclient(): +logger = logging.getLogger(__name__) + + +async def get_usbmux_lockdownclient(): while True: try: - lockdown = create_using_usbmux() + lockdown = await create_using_usbmux() except NoDeviceConnectedError: print("请连接设备后按回车...") input() else: break while True: - lockdown = create_using_usbmux() + lockdown = await create_using_usbmux() if lockdown.all_values.get("PasswordProtected"): print("请解锁设备后按回车...") input() else: break - return create_using_usbmux() + return await create_using_usbmux() + def get_version(lockdown: LockdownClient): return lockdown.all_values.get("ProductVersion") -def get_developer_mode_status(lockdown: LockdownClient): - return lockdown.developer_mode_status -def reveal_developer_mode(lockdown: LockdownClient): - AmfiService(lockdown).create_amfi_show_override_path_file() +async def get_developer_mode_status(lockdown: LockdownClient): + return await lockdown.get_developer_mode_status() + + +async def reveal_developer_mode(lockdown: LockdownClient): + await AmfiService(lockdown).reveal_developer_mode_option_in_ui() -def enable_developer_mode(lockdown: LockdownClient): - AmfiService(lockdown).enable_developer_mode() -def get_serverrsd(): - install_driver_if_required() - if not verify_tunnel_imports(): - exit(1) - return select_device(None) +async def enable_developer_mode(lockdown: LockdownClient): + await AmfiService(lockdown).enable_developer_mode() -async def tunnel(rsd: RemoteServiceDiscoveryService, queue: multiprocessing.Queue): - async with start_tunnel(rsd, None) as tunnel_result: - queue.put((tunnel_result.address, tunnel_result.port)) +async def create_tunnel_from_lockdown(queue: multiprocessing.Queue): + lockdown = await get_usbmux_lockdownclient() + proxy = await CoreDeviceTunnelProxy.create(lockdown) + logger.info("CoreDeviceTunnelProxy created, starting TCP tunnel") + async with proxy.start_tcp_tunnel() as tunnel_result: + queue.put({ + "status": "ok", + "address": tunnel_result.address, + "port": tunnel_result.port, + "protocol": "TCP", + }) + logger.info("tunnel established, address=%s port=%s", tunnel_result.address, tunnel_result.port) await tunnel_result.client.wait_closed() diff --git a/driver/location.py b/driver/location.py index 6a602fc..bd68f23 100644 --- a/driver/location.py +++ b/driver/location.py @@ -1,7 +1,11 @@ -from pymobiledevice3.cli.developer import LocationSimulation +from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation -def set_location(dvt, lat: float, lng: float): - LocationSimulation(dvt).set(lat, lng) -def clear_location(dvt): - LocationSimulation(dvt).clear() +async def set_location(dvt, lat: float, lng: float): + async with LocationSimulation(dvt) as loc: + await loc.set(lat, lng) + + +async def clear_location(dvt): + async with LocationSimulation(dvt) as loc: + await loc.clear() diff --git a/init/init.py b/init/init.py index de453bf..9505ba9 100644 --- a/init/init.py +++ b/init/init.py @@ -4,8 +4,8 @@ from driver import connect -def init(): - # check if root on mac or Administrator on windows + +async def init(): if sys.platform == "win32": if not ctypes.windll.shell32.IsUserAnAdmin(): print("请以管理员权限运行") @@ -18,19 +18,17 @@ def init(): print("仅支持macOS和Windows") sys.exit(1) - # get lockdown client - lockdown = connect.get_usbmux_lockdownclient() + lockdown = await connect.get_usbmux_lockdownclient() - # check version version = connect.get_version(lockdown) print(f"Your system version is {version}") - if version.split(".")[0] < "17": - print(f"仅支持17及以上版本") + major = int(version.split(".")[0]) if version else 0 + if major < 17: + print("仅支持iOS 17及以上版本") sys.exit(1) - # check developer mode status - developer_mode_status = connect.get_developer_mode_status(lockdown) + developer_mode_status = await connect.get_developer_mode_status(lockdown) if not developer_mode_status: - connect.reveal_developer_mode(lockdown) + await connect.reveal_developer_mode(lockdown) print("您未开启开发者模式,请打开设备的 设置-隐私与安全性-开发者模式 来开启,开启后需要重启并输入密码,完成后再次运行此程序") - sys.exit(1) \ No newline at end of file + sys.exit(1) diff --git a/init/tunnel.py b/init/tunnel.py index 5119d90..767ea25 100644 --- a/init/tunnel.py +++ b/init/tunnel.py @@ -1,20 +1,35 @@ import asyncio import multiprocessing +from queue import Empty from driver import connect + def tunnel_proc(queue: multiprocessing.Queue): - server_rsd = connect.get_serverrsd() - asyncio.run(connect.tunnel(server_rsd, queue)) + try: + asyncio.run(connect.create_tunnel_from_lockdown(queue)) + except Exception as exc: + queue.put({ + "status": "error", + "error": repr(exc), + }) def tunnel(): - # start the tunnel in another process queue = multiprocessing.Queue() process = multiprocessing.Process(target=tunnel_proc, args=(queue,)) process.start() - - # get the address and port of the tunnel - address, port = queue.get() - return process, address, port \ No newline at end of file + while True: + try: + result = queue.get(timeout=1) + except Empty: + if not process.is_alive(): + raise RuntimeError("Tunnel process exited before returning connection info") + continue + + if result.get("status") == "ok": + return process, result["address"], result["port"] + + process.join(timeout=1) + raise RuntimeError(f"Tunnel setup failed: {result.get('error', 'unknown error')}") diff --git a/main.py b/main.py index cb273db..40db037 100644 --- a/main.py +++ b/main.py @@ -2,11 +2,11 @@ import logging import coloredlogs import os +import asyncio -from driver import location - -from pymobiledevice3.cli.remote import RemoteServiceDiscoveryService -from pymobiledevice3.cli.developer import DvtSecureSocketProxyService +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService +from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider +from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation from init import init from init import tunnel @@ -19,7 +19,6 @@ debug = os.environ.get("DEBUG", False) -# set logging level coloredlogs.install(level=logging.INFO) logging.getLogger('wintun').setLevel(logging.DEBUG if debug else logging.WARNING) logging.getLogger('quic').setLevel(logging.DEBUG if debug else logging.WARNING) @@ -32,10 +31,29 @@ logging.getLogger('blib2to3.pgen2.driver').setLevel(logging.DEBUG if debug else logging.WARNING) logging.getLogger('urllib3.connectionpool').setLevel(logging.DEBUG if debug else logging.WARNING) +_cleanup_address = None +_cleanup_port = None +_cleanup_process = None -def main(): - # set level +async def _do_clear_location(): + if _cleanup_address is None: + return + try: + async with RemoteServiceDiscoveryService((_cleanup_address, _cleanup_port)) as rsd: + async with DvtProvider(rsd) as dvt: + async with LocationSimulation(dvt) as loc: + await asyncio.wait_for(loc.clear(), timeout=5.0) + print("定位已清除") + except asyncio.TimeoutError: + print("清除定位超时,请重启手机恢复") + except Exception as e: + print(f"清除定位时出错: {e},请重启手机恢复") + + +async def _async_main(): + global _cleanup_address, _cleanup_port, _cleanup_process + logger = logging.getLogger(__name__) coloredlogs.install(level=logging.INFO) logger.setLevel(logging.INFO) @@ -43,51 +61,51 @@ def main(): logger.setLevel(logging.DEBUG) coloredlogs.install(level=logging.DEBUG) - init.init() + await init.init() logger.info("init done") - # start the tunnel in another process logger.info("starting tunnel") original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) - process, address, port = tunnel.tunnel() - signal.signal(signal.SIGINT, original_sigint_handler) - logger.info("tunnel started") try: - logger.debug(f"tunnel address: {address}, port: {port}") - - # get route - loc = route.get_route() - logger.info(f"got route from {config.config.routeConfig}") - - - with RemoteServiceDiscoveryService((address, port)) as rsd: - with DvtSecureSocketProxyService(rsd) as dvt: - try: - print(f"已开始模拟跑步,速度大约为 {config.config.v} m/s") - print("会无限循环,按 Ctrl+C 退出") - print("请勿直接关闭窗口,否则无法还原正常定位") - run.run(dvt, loc, config.config.v) - except KeyboardInterrupt: - logger.debug("get KeyboardInterrupt (inner)") - logger.debug(f"Is process alive? {process.is_alive()}") - finally: - logger.debug(f"Is process alive? {process.is_alive()}") - logger.debug("Start to clear location") - location.clear_location(dvt) - logger.info("Location cleared") + _cleanup_process, _cleanup_address, _cleanup_port = tunnel.tunnel() + except RuntimeError as exc: + logger.error("failed to start tunnel: %s", exc) + print(f"启动隧道失败:{exc}") + return + finally: + signal.signal(signal.SIGINT, original_sigint_handler) + logger.info("tunnel started") + logger.debug(f"tunnel address: {_cleanup_address}, port: {_cleanup_port}") + loc = route.get_route() + logger.info(f"got route from {config.config.routeConfig}") + + async with RemoteServiceDiscoveryService((_cleanup_address, _cleanup_port)) as rsd: + async with DvtProvider(rsd) as dvt: + async with LocationSimulation(dvt) as loc_sim: + print(f"已开始模拟跑步,速度大约为 {config.config.v} m/s") + print("会无限循环,按 Ctrl+C 退出") + print("请勿直接关闭窗口,否则无法还原正常定位") + await run.run(loc_sim, loc, config.config.v) + + +def main(): + try: + asyncio.run(_async_main()) except KeyboardInterrupt: - logger.debug("get KeyboardInterrupt (outer)") - finally: - # stop the tunnel process - logger.debug(f"Is process alive? {process.is_alive()}") - logger.debug("terminating tunnel process") - process.terminate() - logger.info("tunnel process terminated") - print("Bye") - - - + pass + + if _cleanup_process is not None: + print("\n正在清除定位...") + try: + asyncio.run(_do_clear_location()) + except Exception: + pass + _cleanup_process.terminate() + _cleanup_process.join(timeout=5) + print("Bye") + + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/requirements.txt b/requirements.txt index a35c754..52b7a18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -pymobiledevice3==2.46.1 -PyYAML==6.0.1 -geopy==2.4.1 +pymobiledevice3>=9.12.0 +PyYAML>=6.0 +geopy>=2.4 diff --git a/run.py b/run.py index 917299e..3876855 100644 --- a/run.py +++ b/run.py @@ -7,10 +7,10 @@ import math import time import random +import asyncio from geopy.distance import geodesic -from driver import location def bd09Towgs84(position): wgs_p = {} @@ -57,20 +57,17 @@ def transform_lon(x, y): wgs_p["lng"] = gcj_lng * 2 - gcj_lng - d_lng return wgs_p -# get the ditance according to the latitude and longitude + def geodistance(p1, p2): - return geodesic((p1["lat"],p1["lng"]),(p2["lat"],p2["lng"])).m + return geodesic((p1["lat"], p1["lng"]), (p2["lat"], p2["lng"])).m + def smooth(start, end, i): - import math - i = (i-start)/(end-start)*math.pi - return math.sin(i)**2 + i = (i - start) / (end - start) * math.pi + return math.sin(i) ** 2 + def randLoc(loc: list, d=0.000025, n=5): - import random - import time - import math - # deepcopy loc result = [] for i in loc: result.append(i.copy()) @@ -83,73 +80,77 @@ def randLoc(loc: list, d=0.000025, n=5): center["lng"] /= len(result) random.seed(time.time()) for i in range(n): - start = int(i*len(result)/n) - end = int((i+1)*len(result)/n) - offset = (2*random.random()-1) * d + start = int(i * len(result) / n) + end = int((i + 1) * len(result) / n) + offset = (2 * random.random() - 1) * d for j in range(start, end): distance = math.sqrt( - (result[j]["lat"]-center["lat"])**2 + (result[j]["lng"]-center["lng"])**2 + (result[j]["lat"] - center["lat"]) ** 2 + (result[j]["lng"] - center["lng"]) ** 2 ) if 0 == distance: continue - result[j]["lat"] += (result[j]["lat"]-center["lat"])/distance*offset*smooth(start, end, j) - result[j]["lng"] += (result[j]["lng"]-center["lng"])/distance*offset*smooth(start, end, j) - start = int(i*len(result)/n) + result[j]["lat"] += (result[j]["lat"] - center["lat"]) / distance * offset * smooth(start, end, j) + result[j]["lng"] += (result[j]["lng"] - center["lng"]) / distance * offset * smooth(start, end, j) + start = int(i * len(result) / n) end = len(result) - offset = (2*random.random()-1) * d + offset = (2 * random.random() - 1) * d for j in range(start, end): distance = math.sqrt( - (result[j]["lat"]-center["lat"])**2 + (result[j]["lng"]-center["lng"])**2 + (result[j]["lat"] - center["lat"]) ** 2 + (result[j]["lng"] - center["lng"]) ** 2 ) if 0 == distance: continue - result[j]["lat"] += (result[j]["lat"]-center["lat"])/distance*offset*smooth(start, end, j) - result[j]["lng"] += (result[j]["lng"]-center["lng"])/distance*offset*smooth(start, end, j) + result[j]["lat"] += (result[j]["lat"] - center["lat"]) / distance * offset * smooth(start, end, j) + result[j]["lng"] += (result[j]["lng"] - center["lng"]) / distance * offset * smooth(start, end, j) return result + def fixLockT(loc: list, v, dt): fixedLoc = [] t = 0 T = [] - T.append(geodistance(loc[1],loc[0])/v) + T.append(geodistance(loc[1], loc[0]) / v) a = loc[0].copy() b = loc[1].copy() j = 0 while t < T[0]: - xa = a["lat"] + j*(b["lat"]-a["lat"])/(max(1, int(T[0]/dt))) - xb = a["lng"] + j*(b["lng"]-a["lng"])/(max(1, int(T[0]/dt))) + xa = a["lat"] + j * (b["lat"] - a["lat"]) / (max(1, int(T[0] / dt))) + xb = a["lng"] + j * (b["lng"] - a["lng"]) / (max(1, int(T[0] / dt))) fixedLoc.append({"lat": xa, "lng": xb}) j += 1 t += dt for i in range(1, len(loc)): - T.append(geodistance(loc[(i+1)%len(loc)],loc[i])/v + T[-1]) + T.append(geodistance(loc[(i + 1) % len(loc)], loc[i]) / v + T[-1]) a = loc[i].copy() - b = loc[(i+1)%len(loc)].copy() + b = loc[(i + 1) % len(loc)].copy() j = 0 while t < T[i]: - xa = a["lat"] + j*(b["lat"]-a["lat"])/(max(1, int((T[i]-T[i-1])/dt))) - xb = a["lng"] + j*(b["lng"]-a["lng"])/(max(1, int((T[i]-T[i-1])/dt))) + xa = a["lat"] + j * (b["lat"] - a["lat"]) / (max(1, int((T[i] - T[i - 1]) / dt))) + xb = a["lng"] + j * (b["lng"] - a["lng"]) / (max(1, int((T[i] - T[i - 1]) / dt))) fixedLoc.append({"lat": xa, "lng": xb}) j += 1 t += dt return fixedLoc -def run1(dvt, loc: list, v, dt=0.2): + +async def run1(loc_sim, loc: list, v, dt=0.2): fixedLoc = fixLockT(loc, v, dt) nList = (5, 6, 7, 8, 9) - n = nList[random.randint(0, len(nList)-1)] - fixedLoc = randLoc(fixedLoc, n=n) # a path will be divided into n parts for random route + n = nList[random.randint(0, len(nList) - 1)] + fixedLoc = randLoc(fixedLoc, n=n) clock = time.time() for i in fixedLoc: - # utils.setLoc(bd09Towgs84(i)) - location.set_location(dvt, **bd09Towgs84(i)) - while time.time()-clock < dt: - pass + pos = bd09Towgs84(i) + await loc_sim.set(pos["lat"], pos["lng"]) + elapsed = time.time() - clock + if elapsed < dt: + await asyncio.sleep(dt - elapsed) clock = time.time() -def run(dvt, loc: list, v, d=15): + +async def run(loc_sim, loc: list, v, d=15): random.seed(time.time()) while True: - vRand = 1000/(1000/v-(2*random.random()-1)*d) - run1(dvt, loc, vRand) - print("跑完一圈了") \ No newline at end of file + vRand = 1000 / (1000 / v - (2 * random.random() - 1) * d) + await run1(loc_sim, loc, vRand) + print("跑完一圈了")