Skip to content

Feature/dtx service lifecycle#1625

Open
tux-mind wants to merge 16 commits into
doronz88:masterfrom
tux-mind:feature/dtx_service_lifecycle
Open

Feature/dtx service lifecycle#1625
tux-mind wants to merge 16 commits into
doronz88:masterfrom
tux-mind:feature/dtx_service_lifecycle

Conversation

@tux-mind

Copy link
Copy Markdown
Contributor

DTXChannel and DTXService lifecycle with callbacks.
Duplicates #1611

It also contains some minor bugfixes 😊
Let me know if it's ok or you wish to discard or re adapt it.

For community

⬇️ Please click the 👍 reaction instead of leaving a +1 or 👍 comment

@doronz88 doronz88 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks great but it broke all taps. Please fix the requested change :)

Comment thread pymobiledevice3/dtx/connection.py Outdated

async def __aexit__(self, exc_type, exc_val, exc_tb):
await (await self._service_ref()).stop()
with suppress(ConnectionTerminatedError):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, actually this piece of code should also be refactored to be like DeviceInfo service and all others, that instead of using _service_ref(), will just trigger connect only when explicitly asked or on enter. I'll fix this in a later PR. Making a service ref just to immediately stop it is funny though :P

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, today I went a bit over the Tap class and it looks like it can benefit from a refactoring.
I would make Tap a subclass of DtxService[TapService] with an @abstractmethod _getConfig(self) which subclasses would implement to provide the setConfig: argument.
I still have to fully understand the different use-cases of the received messages: when is it expected that the received notifications / data are NSKeyedArchives?
Are those 2 different types of Taps? One that automatically decodes received data using unarchive and the other that provides the messages as recevied?

DTXChannel automatically decodesnotification events with NSKeyedArchiver ( or as plist as fallback ) while data events are passed over as raw bytes.

Still, another PR as you say 😊

Comment thread pymobiledevice3/dtx/exceptions.py
Comment thread pymobiledevice3/services/dvt/instruments/tap.py Outdated
@tux-mind

Copy link
Copy Markdown
Contributor Author

Hey, thanks for the review, let me take care of this.
I'm sorry for the breaking changes.

@tux-mind

Copy link
Copy Markdown
Contributor Author

All good, can you please double check?

@doronz88

Copy link
Copy Markdown
Owner

Looks like taps are broken. Did not debug it myself, but you can test it:

pymobiledevice3 developer dvt sysmon process monitor pid 1

@tux-mind

Copy link
Copy Markdown
Contributor Author

Thanks for pointing that out!

It should be ok now, I've tested both har and oslog in addition to sysmon.
There is a slight issue with oslog tho: the service sends events so rapidly ( or the parsing takes so long ) that when exiting there still are unprocessed events:

^C2026-03-23 00:37:39 MacBook-Pro.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(1)[46410] WARNING Channel 'com.apple.instruments.server.services.activitytracetap' shutting down (context exit) with 4 unprocessed message(s) in queue

let me know if this looks good or needs more care 😊
Have a great week ahead 🎉

@doronz88

Copy link
Copy Markdown
Owner

Looks like the wrong exception is raised. It raises TimeoutError on device disconnects from taps instead of ConnectionTerminatedError

@tux-mind

Copy link
Copy Markdown
Contributor Author

Thank you for finding that out!
Fixed, tested with oslog and sysmon process monitor pid 😊

There was a subtle bug that polluted the exception traceback, now the original traceback is preserved and if some weird exception occurs in the dtx code you get the original traceback in the CLI.

@doronz88

Copy link
Copy Markdown
Owner

Still getting the wrong error from the same command:

2026-03-26 07:48:44 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9)[91660] ERROR DTX reader exiting with error: [Errno 60] Operation timed out
╭─────────────────────────────────────── Traceback (most recent call last) ────────────────────────────────────────╮
│ /Users/user/.local/share/uv/tools/pymobiledevice3/lib/python3.14/site-packages/typer_injector/_inject.py:136 in  │
│ wrapper                                                                                                          │
│                                                                                                                  │
│ /Users/user/.local/share/uv/tools/pymobiledevice3/lib/python3.14/site-packages/typer_injector/_inject.py:107 in  │
│ invoke_with_dependencies                                                                                         │
│                                                                                                                  │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/cli/cli_common.py:140 in wrapper                                 │
│                                                                                                                  │
│   137 │   def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:                                                   │
│   138 │   │   task = cli_loop.create_task(func(*args, **kwargs))                                                 │
│   139 │   │   try:                                                                                               │
│ ❱ 140 │   │   │   return cli_loop.run_until_complete(task)                                                       │
│   141 │   │   except KeyboardInterrupt:                                                                          │
│   142 │   │   │   # Ensure graceful coroutine finalization on Ctrl-C; otherwise Python                           │
│   143 │   │   │   # may report "coroutine ignored GeneratorExit" during GC shutdown.                             │
│                                                                                                                  │
│ /Users/user/.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/asyncio/base_events.py:719   │
│ in run_until_complete                                                                                            │
│                                                                                                                  │
│    716 │   │   if not future.done():                                                                             │
│    717 │   │   │   raise RuntimeError('Event loop stopped before Future completed.')                             │
│    718 │   │                                                                                                     │
│ ❱  719 │   │   return future.result()                                                                            │
│    720 │                                                                                                         │
│    721 │   def stop(self):                                                                                       │
│    722 │   │   """Stop running the event loop.                                                                   │
│                                                                                                                  │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/cli/developer/dvt/sysmon/process.py:278 in                       │
│ sysmon_process_monitor_pid                                                                                       │
│                                                                                                                  │
│   275 │   │   output_file = stack.enter_context(open(output, "w")) if output else None                           │
│   276 │   │                                                                                                      │
│   277 │   │   async with DvtProvider(service_provider) as dvt, await Sysmontap.create(dvt, int                   │
│ ❱ 278 │   │   │   async for process_snapshot in sysmon.iter_processes():                                         │
│   279 │   │   │   │   count += 1                                                                                 │
│   280 │   │   │   │                                                                                              │
│   281 │   │   │   │   if count < 2:                                                                              │
│                                                                                                                  │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/services/dvt/instruments/sysmontap.py:45 in iter_processes       │
│                                                                                                                  │
│   42 │   │   return self.__config__                                                                              │
│   43 │                                                                                                           │
│   44 │   async def iter_processes(self):                                                                         │
│ ❱ 45 │   │   async for row in self:                                                                              │
│   46 │   │   │   if isinstance(row, dict):                                                                       │
│   47 │   │   │   │   row = [row]                                                                                 │
│   48 │   │   │   for event in row:                                                                               │
│                                                                                                                  │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/services/dvt/instruments/tap.py:99 in objects                    │
│                                                                                                                  │
│    96 │                                                                                                          │
│    97 │   async def objects(self) -> AsyncGenerator[Any, None]:                                                  │
│    98 │   │   """Yield notifications and parse data messages as archived objects."""                             │
│ ❱  99 │   │   async for kind, payload in self.messages():                                                        │
│   100 │   │   │   if kind == "notification":                                                                     │
│   101 │   │   │   │   yield payload                                                                              │
│   102 │   │   │   │   continue                                                                                   │
│                                                                                                                  │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/services/dvt/instruments/tap.py:83 in messages                   │
│                                                                                                                  │
│    80 │   │   except asyncio.QueueShutDown:                                                                      │
│    81 │   │   │   ex = self.service.stop_exception                                                               │
│    82 │   │   │   if ex is not None:                                                                             │
│ ❱  83 │   │   │   │   raise ex from getattr(ex, "__cause__", None)                                               │
│    84 │                                                                                                          │
│    85 │   async def notifications(self) -> AsyncGenerator[Any, None]:                                            │
│    86 │   │   """Yield notification messages from the TAP, ignoring data messages."""                            │
│                                                                                                                  │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/dtx/_reader.py:81 in _process_incoming_fragments                 │
│                                                                                                                  │
│    78 │   │   """Background task: read fragments until the connection closes or errors."""                       │
│    79 │   │   try:                                                                                               │
│    80 │   │   │   while not self._closed:                                                                        │
│ ❱  81 │   │   │   │   fragment = await DTXFragment.read(self._reader)                                            │
│    82 │   │   │   │                                                                                              │
│    83 │   │   │   │   if fragment.count == 1:                                                                    │
│    84 │   │   │   │   │   # Single-fragment message: process immediately.                                        │
│                                                                                                                  │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/dtx/fragment.py:45 in read                                       │
│                                                                                                                  │
│    42 │   @staticmethod                                                                                          │
│    43 │   async def read(stream: asyncio.StreamReader) -> DTXFragment:                                           │
│    44 │   │   """Parse a DTX fragment from *stream* and return a DTXFragment object."""                          │
│ ❱  45 │   │   header = dtx_fragment_header.parse(await stream.readexactly(FRAGMENT_HEADER_MIN_                   │
│    46 │   │                                                                                                      │
│    47 │   │   if header.index >= header.count:                                                                   │
│    48 │   │   │   raise DTXProtocolError(                                                                        │
│                                                                                                                  │
│ /Users/user/.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/asyncio/streams.py:769 in    │
│ readexactly                                                                                                      │
│                                                                                                                  │
│   766 │   │   │   │   self._buffer.clear()                                                                       │
│   767 │   │   │   │   raise exceptions.IncompleteReadError(incomplete, n)                                        │
│   768 │   │   │                                                                                                  │
│ ❱ 769 │   │   │   await self._wait_for_data('readexactly')                                                       │
│   770 │   │                                                                                                      │
│   771 │   │   if len(self._buffer) == n:                                                                         │
│   772 │   │   │   data = bytes(self._buffer)                                                                     │
│                                                                                                                  │
│ /Users/user/.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/asyncio/streams.py:539 in    │
│ _wait_for_data                                                                                                   │
│                                                                                                                  │
│   536 │   │                                                                                                      │
│   537 │   │   self._waiter = self._loop.create_future()                                                          │
│   538 │   │   try:                                                                                               │
│ ❱ 539 │   │   │   await self._waiter                                                                             │
│   540 │   │   finally:                                                                                           │
│   541 │   │   │   self._waiter = None                                                                            │
│   542                                                                                                            │
│                                                                                                                  │
│ /Users/user/.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/asyncio/selector_events.py:1 │
│ 009 in _read_ready__data_received                                                                                │
│                                                                                                                  │
│   1006 │   │   if self._conn_lost:                                                                               │
│   1007 │   │   │   return                                                                                        │
│   1008 │   │   try:                                                                                              │
│ ❱ 1009 │   │   │   data = self._sock.recv(self.max_size)                                                         │
│   1010 │   │   except (BlockingIOError, InterruptedError):                                                       │
│   1011 │   │   │   return                                                                                        │
│   1012 │   │   except (SystemExit, KeyboardInterrupt):                                                           │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
TimeoutError: [Errno 60] Operation timed out

@doronz88

Copy link
Copy Markdown
Owner

If you prefer, I can merge your bugfixes seperately first

@tux-mind

Copy link
Copy Markdown
Contributor Author

Hey, thanks for checking that out, for some reason I get errno 56 on my end, that's why it didn't cover the issue you're facing.

The proper fix would be to isolate socket operations ( connect, read, write ) in try/catch blocks, retry the operation up to a certain number of maximum attempts and then fire a module-level exception.
This would cover any Exception that is fired for interacting with the DTX connection and correctly identify them as a connection terminated / invalid.

If you want I can take care of this improvement after April 8.

Let me know if you think that other stuff is still missing from this PR, I'll find some time to improve it as you want 😊

@doronz88

doronz88 commented Mar 30, 2026

Copy link
Copy Markdown
Owner

Looks like the latest commit did not fix it:

2026-03-30 09:28:41 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9)[21815] ERROR DTX reader exiting with error: [Errno 60] Operation timed out
2026-03-30 09:28:41 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[21815] DEBUG Stopping reader task: connection closed: reader exiting
2026-03-30 09:28:41 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(0)[21815] DEBUG Stopping reader task: connection closed: reader exiting
╭─────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ /Users/user/.local/share/uv/tools/pymobiledevice3/lib/python3.14/site-packages/typer_injector/_inject.py:136 in wrapper                                                                                                                  │
│                                                                                                                                                                                                                                          │
│ /Users/user/.local/share/uv/tools/pymobiledevice3/lib/python3.14/site-packages/typer_injector/_inject.py:107 in invoke_with_dependencies                                                                                                 │
│                                                                                                                                                                                                                                          │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/cli/cli_common.py:140 in wrapper                                                                                                                                                         │
│                                                                                                                                                                                                                                          │
│   137 │   def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:                                                                                                                                                                           │
│   138 │   │   task = cli_loop.create_task(func(*args, **kwargs))                                                                                                                                                                         │
│   139 │   │   try:                                                                                                                                                                                                                       │
│ ❱ 140 │   │   │   return cli_loop.run_until_complete(task)                                                                                                                                                                               │
│   141 │   │   except KeyboardInterrupt:                                                                                                                                                                                                  │
│   142 │   │   │   # Ensure graceful coroutine finalization on Ctrl-C; otherwise Python                                                                                                                                                   │
│   143 │   │   │   # may report "coroutine ignored GeneratorExit" during GC shutdown.                                                                                                                                                     │
│                                                                                                                                                                                                                                          │
│ /Users/user/.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/asyncio/base_events.py:719 in run_until_complete                                                                                                     │
│                                                                                                                                                                                                                                          │
│    716 │   │   if not future.done():                                                                                                                                                                                                     │
│    717 │   │   │   raise RuntimeError('Event loop stopped before Future completed.')                                                                                                                                                     │
│    718 │   │                                                                                                                                                                                                                             │
│ ❱  719 │   │   return future.result()                                                                                                                                                                                                    │
│    720 │                                                                                                                                                                                                                                 │
│    721 │   def stop(self):                                                                                                                                                                                                               │
│    722 │   │   """Stop running the event loop.                                                                                                                                                                                           │
│                                                                                                                                                                                                                                          │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/cli/developer/dvt/sysmon/process.py:278 in sysmon_process_monitor_pid                                                                                                                    │
│                                                                                                                                                                                                                                          │
│   275 │   │   output_file = stack.enter_context(open(output, "w")) if output else None                                                                                                                                                   │
│   276 │   │                                                                                                                                                                                                                              │
│   277 │   │   async with DvtProvider(service_provider) as dvt, await Sysmontap.create(dvt, int                                                                                                                                           │
│ ❱ 278 │   │   │   async for process_snapshot in sysmon.iter_processes():                                                                                                                                                                 │
│   279 │   │   │   │   count += 1                                                                                                                                                                                                         │
│   280 │   │   │   │                                                                                                                                                                                                                      │
│   281 │   │   │   │   if count < 2:                                                                                                                                                                                                      │
│                                                                                                                                                                                                                                          │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/services/dvt/instruments/sysmontap.py:45 in iter_processes                                                                                                                               │
│                                                                                                                                                                                                                                          │
│   42 │   │   return self.__config__                                                                                                                                                                                                      │
│   43 │                                                                                                                                                                                                                                   │
│   44 │   async def iter_processes(self):                                                                                                                                                                                                 │
│ ❱ 45 │   │   async for row in self:                                                                                                                                                                                                      │
│   46 │   │   │   if isinstance(row, dict):                                                                                                                                                                                               │
│   47 │   │   │   │   row = [row]                                                                                                                                                                                                         │
│   48 │   │   │   for event in row:                                                                                                                                                                                                       │
│                                                                                                                                                                                                                                          │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/services/dvt/instruments/tap.py:99 in objects                                                                                                                                            │
│                                                                                                                                                                                                                                          │
│    96 │                                                                                                                                                                                                                                  │
│    97 │   async def objects(self) -> AsyncGenerator[Any, None]:                                                                                                                                                                          │
│    98 │   │   """Yield notifications and parse data messages as archived objects."""                                                                                                                                                     │
│ ❱  99 │   │   async for kind, payload in self.messages():                                                                                                                                                                                │
│   100 │   │   │   if kind == "notification":                                                                                                                                                                                             │
│   101 │   │   │   │   yield payload                                                                                                                                                                                                      │
│   102 │   │   │   │   continue                                                                                                                                                                                                           │
│                                                                                                                                                                                                                                          │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/services/dvt/instruments/tap.py:83 in messages                                                                                                                                           │
│                                                                                                                                                                                                                                          │
│    80 │   │   except asyncio.QueueShutDown:                                                                                                                                                                                              │
│    81 │   │   │   ex = self.service.stop_exception                                                                                                                                                                                       │
│    82 │   │   │   if ex is not None:                                                                                                                                                                                                     │
│ ❱  83 │   │   │   │   raise ex from getattr(ex, "__cause__", None)                                                                                                                                                                       │
│    84 │                                                                                                                                                                                                                                  │
│    85 │   async def notifications(self) -> AsyncGenerator[Any, None]:                                                                                                                                                                    │
│    86 │   │   """Yield notification messages from the TAP, ignoring data messages."""                                                                                                                                                    │
│                                                                                                                                                                                                                                          │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/dtx/_reader.py:81 in _process_incoming_fragments                                                                                                                                         │
│                                                                                                                                                                                                                                          │
│    78 │   │   """Background task: read fragments until the connection closes or errors."""                                                                                                                                               │
│    79 │   │   try:                                                                                                                                                                                                                       │
│    80 │   │   │   while not self._closed:                                                                                                                                                                                                │
│ ❱  81 │   │   │   │   fragment = await DTXFragment.read(self._reader)                                                                                                                                                                    │
│    82 │   │   │   │                                                                                                                                                                                                                      │
│    83 │   │   │   │   if fragment.count == 1:                                                                                                                                                                                            │
│    84 │   │   │   │   │   # Single-fragment message: process immediately.                                                                                                                                                                │
│                                                                                                                                                                                                                                          │
│ /Users/user/dev/pymobiledevice3/pymobiledevice3/dtx/fragment.py:45 in read                                                                                                                                                               │
│                                                                                                                                                                                                                                          │
│    42 │   @staticmethod                                                                                                                                                                                                                  │
│    43 │   async def read(stream: asyncio.StreamReader) -> DTXFragment:                                                                                                                                                                   │
│    44 │   │   """Parse a DTX fragment from *stream* and return a DTXFragment object."""                                                                                                                                                  │
│ ❱  45 │   │   header = dtx_fragment_header.parse(await stream.readexactly(FRAGMENT_HEADER_MIN_                                                                                                                                           │
│    46 │   │                                                                                                                                                                                                                              │
│    47 │   │   if header.index >= header.count:                                                                                                                                                                                           │
│    48 │   │   │   raise DTXProtocolError(                                                                                                                                                                                                │
│                                                                                                                                                                                                                                          │
│ /Users/user/.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/asyncio/streams.py:769 in readexactly                                                                                                                │
│                                                                                                                                                                                                                                          │
│   766 │   │   │   │   self._buffer.clear()                                                                                                                                                                                               │
│   767 │   │   │   │   raise exceptions.IncompleteReadError(incomplete, n)                                                                                                                                                                │
│   768 │   │   │                                                                                                                                                                                                                          │
│ ❱ 769 │   │   │   await self._wait_for_data('readexactly')                                                                                                                                                                               │
│   770 │   │                                                                                                                                                                                                                              │
│   771 │   │   if len(self._buffer) == n:                                                                                                                                                                                                 │
│   772 │   │   │   data = bytes(self._buffer)                                                                                                                                                                                             │
│                                                                                                                                                                                                                                          │
│ /Users/user/.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/asyncio/streams.py:539 in _wait_for_data                                                                                                             │
│                                                                                                                                                                                                                                          │
│   536 │   │                                                                                                                                                                                                                              │
│   537 │   │   self._waiter = self._loop.create_future()                                                                                                                                                                                  │
│   538 │   │   try:                                                                                                                                                                                                                       │
│ ❱ 539 │   │   │   await self._waiter                                                                                                                                                                                                     │
│   540 │   │   finally:                                                                                                                                                                                                                   │
│   541 │   │   │   self._waiter = None                                                                                                                                                                                                    │
│   542                                                                                                                                                                                                                                    │
│                                                                                                                                                                                                                                          │
│ /Users/user/.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/asyncio/selector_events.py:1009 in _read_ready__data_received                                                                                        │
│                                                                                                                                                                                                                                          │
│   1006 │   │   if self._conn_lost:                                                                                                                                                                                                       │
│   1007 │   │   │   return                                                                                                                                                                                                                │
│   1008 │   │   try:                                                                                                                                                                                                                      │
│ ❱ 1009 │   │   │   data = self._sock.recv(self.max_size)                                                                                                                                                                                 │
│   1010 │   │   except (BlockingIOError, InterruptedError):                                                                                                                                                                               │
│   1011 │   │   │   return                                                                                                                                                                                                                │
│   1012 │   │   except (SystemExit, KeyboardInterrupt):                                                                                                                                                                                   │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
TimeoutError: [Errno 60] Operation timed out

@doronz88

Copy link
Copy Markdown
Owner

Found the bug, I'm invoking without --tunnel '', making the root exception be InvalidServiceError instead of the timeout exception. We need to ignore the InvalidServiceError while digging into the exception, or alternatively and probably better would be to stop at any of the connection errors

tux-mind added 14 commits March 31, 2026 17:21
PrimitiveInt32 and PrimitiveInt64 are signed integers,
not unsigned.
Created a test which assumes that a Service is notified when
the underlaying DTXConnection is closed and gracefully propagates
the ConnectionTerminatedError to the Service user.
added `aclose` to handle dispose requests from various sources.
Added the ability to `dtx` to register `DTXService`s by specifying
their name, allowing for reuse of the same class with different `IDENTIFIER`s.

Refactored Tap to take fulll advantage of `DtxService` and async generators.
Refactored sysmon and activitytrace to use the new API without major changes.
When calling the `aclose` cleanup methods on resources,
they might attempt to cleanup the very same resources that fired
the exception they have been given as cause.

In such cases, the `aclose` code might trigger a `rise exc` on
the same exception that `aclose` itself has been given ( or in general,
an exception that have been stored somewhere ).

When `raise exc` is called, python prepends the current stacktrace to
the excpetion __traceback__, polluting the stored exception ( the same object ).

In code:
```python
try:
    self.connection.read(...) # original __traceback__ points here
excpetion Exception as e:
    self.exception = e
    with suppress(Exception):
        self.connection.close()

print(e.__traceback__) # will point to self.connection.close()

```
When a device is disconnected, the error we get is `[Errno 65] No route to host` .
@doronz88 doronz88 force-pushed the feature/dtx_service_lifecycle branch from ca51e64 to d6496a4 Compare March 31, 2026 14:23
@doronz88

Copy link
Copy Markdown
Owner

After rebasing, this warning floods:

user@users-Mac-mini-5 ~/dev/pymobiledevice3 feature/dtx_service_lifecycle @ pymobiledevice3 developer dvt sysmon process monitor process -f pid=1
2026-03-31 17:23:52 users-Mac-mini-5.local pymobiledevice3.__main__[25551] WARNING Got an InvalidServiceError. Trying again over tunneld since it is a developer command
Monitoring pid=1, ppid=0, name=launchd
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i649.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25057833735} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i650.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25057857720} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i651.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25057881729} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i652.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25057905722} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i653.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25057929760} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i654.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25057953730} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i655.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25057977606} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i656.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25058001735} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i657.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25058025726} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i658.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25058049768} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i659.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25058073915} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i660.0 c2 type:OBJECT flags:0x0.0x0 payload:{'k': 8, 'heart': 25058097932} aux:[]>
2026-03-31 17:23:53 users-Mac-mini-5.local pymobiledevice3.dtx.connection.DTXConnection(9).channel(2)[25551] WARNING Received message for channel 'com.apple.instruments.server.services.sysmontap' after channel was closed: <DTXMessage: i

…dServiceError`

The `InvalidServiceError` exception might be misleading since we are expected to
encounter it whenever the user started execution without the `--tunnel` optional
even when its needed.
@doronz88

Copy link
Copy Markdown
Owner

@tux-mind Added commits that fixed all issues I encountered. What do you think of them?

@doronz88

doronz88 commented Apr 6, 2026

Copy link
Copy Markdown
Owner

Looks like this branch also has additional erros handling more taps.
Both of these never exist gracefully:

  • pymobiledevice3 developer dvt oslog 1
  • pymobiledevice3 developer dvt core-profile-session parse-live --bsc

@doronz88

doronz88 commented Apr 6, 2026

Copy link
Copy Markdown
Owner

I will still keep f754064 as this commit seems very much detached

@doronz88

doronz88 commented Apr 6, 2026

Copy link
Copy Markdown
Owner

I did merge the other branch meanwhile, tackling the same issue as it currently still improves

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants