AndroidFridaManager 1.9.4__py3-none-any.whl → 1.9.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,4 +2,4 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  __author__ = "Daniel Baier"
5
- __version__ = "1.9.4"
5
+ __version__ = "1.9.6"
@@ -32,7 +32,7 @@ class Job:
32
32
  display_name: Human-readable name for UI display.
33
33
  hooks_registry: List of methods/functions this job hooks (for conflict detection).
34
34
  priority: Job priority (lower = higher priority, default 50).
35
- state: Current state ("initialized", "running", "stopping").
35
+ state: Current state ("initialized", "running", "stopping", "error", "stopped").
36
36
  started_at: Timestamp when job was started (None if not started).
37
37
  """
38
38
 
@@ -75,6 +75,50 @@ class Job:
75
75
  self.priority = priority
76
76
  self.started_at: Optional[datetime.datetime] = None
77
77
 
78
+ # State propagation for callers to wait on job readiness
79
+ self._ready_event = threading.Event()
80
+ self._error_message: Optional[str] = None
81
+
82
+ def _set_state(self, new_state: str, error_msg: Optional[str] = None) -> None:
83
+ """Set state and signal ready event if terminal state reached.
84
+
85
+ Args:
86
+ new_state: New state value ("initialized", "running", "error", "stopping", "stopped").
87
+ error_msg: Optional error message when transitioning to "error" state.
88
+ """
89
+ self.state = new_state
90
+ if error_msg:
91
+ self._error_message = error_msg
92
+ # Signal ready event when job reaches a terminal state (running, error, stopped)
93
+ if new_state in ("running", "error", "stopped"):
94
+ self._ready_event.set()
95
+
96
+ def wait_until_ready(self, timeout: float = 10.0) -> bool:
97
+ """Wait for job to reach running or error state.
98
+
99
+ Blocks until the job thread signals that hooks have been loaded
100
+ successfully (state="running") or an error occurred (state="error").
101
+
102
+ Args:
103
+ timeout: Maximum seconds to wait (default: 10.0).
104
+
105
+ Returns:
106
+ True if job is running successfully, False if error or timeout.
107
+ """
108
+ if self._ready_event.wait(timeout=timeout):
109
+ return self.state == "running"
110
+ # Timeout occurred
111
+ self._error_message = f"Timeout waiting for job to start after {timeout}s"
112
+ return False
113
+
114
+ def get_error(self) -> Optional[str]:
115
+ """Get error message if job failed.
116
+
117
+ Returns:
118
+ Error message string if job is in error state, None otherwise.
119
+ """
120
+ return self._error_message
121
+
78
122
 
79
123
  def create_job_script(self):
80
124
  self.instrument(self.process_session)
@@ -93,18 +137,39 @@ class Job:
93
137
 
94
138
 
95
139
  def invoke_handle_hooking(self):
96
- if self.is_script_created == False:
97
- self.instrument(self.process_session)
98
- self.is_script_created = True
99
- self.script.on("message", self.wrap_custom_hooking_handler_with_job_id(self.custom_hooking_handler))
100
- self.script.load()
101
- self.state = "running"
102
- self.logger.info("[+] hooks successfully loaded")
103
-
104
- #if self.is_running_as_thread:
105
- # Keep the thread alive to handle messages until stop_event is set
106
- while not self.stop_event.is_set():
107
- self.stop_event.wait(1) # Sleep for 1 second and check again
140
+ try:
141
+ if self.is_script_created == False:
142
+ self.instrument(self.process_session)
143
+ self.is_script_created = True
144
+ self.script.on("message", self.wrap_custom_hooking_handler_with_job_id(self.custom_hooking_handler))
145
+ self.script.load()
146
+ self._set_state("running")
147
+ self.logger.info("[+] hooks successfully loaded")
148
+
149
+ #if self.is_running_as_thread:
150
+ # Keep the thread alive to handle messages until stop_event is set
151
+ while not self.stop_event.is_set():
152
+ self.stop_event.wait(1) # Sleep for 1 second and check again
153
+ except frida.TransportError as e:
154
+ error_msg = f"TransportError during script load: {e} - target app may have crashed or restarted"
155
+ self._set_state("error", error_msg)
156
+ self.logger.error(f"[-] {error_msg}")
157
+ except frida.InvalidOperationError as e:
158
+ error_msg = f"InvalidOperationError during script load: {e}"
159
+ self._set_state("error", error_msg)
160
+ self.logger.error(f"[-] {error_msg}")
161
+ except frida.ProcessNotFoundError as e:
162
+ error_msg = f"ProcessNotFoundError: Target process no longer exists: {e}"
163
+ self._set_state("error", error_msg)
164
+ self.logger.error(f"[-] {error_msg}")
165
+ except frida.ProtocolError as e:
166
+ error_msg = f"ProtocolError: Connection issue with target: {e}"
167
+ self._set_state("error", error_msg)
168
+ self.logger.error(f"[-] {error_msg}")
169
+ except Exception as e:
170
+ error_msg = f"Unexpected error in hook thread: {type(e).__name__}: {e}"
171
+ self._set_state("error", error_msg)
172
+ self.logger.error(f"[-] {error_msg}")
108
173
 
109
174
 
110
175
  def wrap_custom_hooking_handler_with_job_id(self, handler):
@@ -134,15 +199,51 @@ class Job:
134
199
  raise FridaBasedException("Connection is closed. Probably the target app crashed")
135
200
 
136
201
 
137
- def close_job(self):
138
- self.state = "stopping"
202
+ def close_job(self, timeout: float = 5.0) -> bool:
203
+ """Stop the job and cleanup resources.
204
+
205
+ Uses a staged shutdown approach:
206
+ 1. Set stop_event FIRST to signal thread to exit wait loop
207
+ 2. Wait briefly for thread to notice stop_event
208
+ 3. Try to unload script (may hang if connection broken)
209
+ 4. Set final stopped state
210
+
211
+ Args:
212
+ timeout: Maximum seconds to wait for thread to stop.
213
+ Default 5.0 seconds. Use 0 for no wait.
214
+
215
+ Returns:
216
+ True if job stopped cleanly, False if timed out.
217
+ """
218
+ self._set_state("stopping")
219
+
220
+ # Step 1: Signal thread to exit wait loop FIRST
139
221
  self.stop_event.set()
140
- if self.thread:
141
- self.thread.join()
222
+
223
+ # Step 2: Wait for thread to notice stop_event (short timeout)
224
+ thread_timeout = min(1.0, timeout) if timeout > 0 else 1.0
225
+ timed_out = False
226
+ if self.thread and self.thread.is_alive():
227
+ self.thread.join(timeout=thread_timeout)
228
+ if self.thread.is_alive():
229
+ self.logger.warning(
230
+ f"Job {self.job_id} thread did not stop within {thread_timeout}s"
231
+ )
232
+ timed_out = True
233
+
234
+ # Step 3: Try to unload script (may hang if connection broken)
142
235
  if self.script:
143
- self.script.unload()
144
-
145
- self.logger.info(f"Job {self.job_id} stopped")
236
+ try:
237
+ self.script.unload()
238
+ except Exception as e:
239
+ self.logger.warning(f"Script unload failed (connection may be broken): {e}")
240
+
241
+ # Step 4: Set final state
242
+ self._set_state("stopped")
243
+
244
+ status = "timed out" if timed_out else "stopped"
245
+ self.logger.info(f"Job {self.job_id} {status}")
246
+ return not timed_out
146
247
 
147
248
 
148
249
  def get_id(self):
@@ -424,29 +424,48 @@ class JobManager(object):
424
424
  return None
425
425
 
426
426
 
427
- def stop_jobs(self):
427
+ def stop_jobs(self, timeout_per_job: float = 3.0) -> dict:
428
+ """Stop all running jobs.
429
+
430
+ Args:
431
+ timeout_per_job: Maximum seconds to wait per job.
432
+
433
+ Returns:
434
+ Dictionary mapping job_id to success status.
435
+ """
436
+ results = {}
428
437
  jobs_to_stop = [job_id for job_id, job in self.jobs.items() if job.state == "running"]
438
+
429
439
  for job_id in jobs_to_stop:
430
440
  try:
431
- self.logger.info('[job manager] Job: {0} - Stopping'.format(job_id))
432
- self.stop_job_with_id(job_id)
441
+ self.logger.info(f'[job manager] Job: {job_id} - Stopping')
442
+ results[job_id] = self.stop_job_with_id(job_id, timeout=timeout_per_job)
433
443
  except frida.InvalidOperationError:
434
- self.logger.error('[job manager] Job: {0} - An error occurred stopping job. Device may '
435
- 'no longer be available.'.format(job_id))
444
+ self.logger.error(f'[job manager] Job: {job_id} - Error stopping')
445
+ results[job_id] = False
446
+
447
+ return results
436
448
 
437
449
 
438
- def stop_job_with_id(self, job_id: str) -> None:
450
+ def stop_job_with_id(self, job_id: str, timeout: float = 5.0) -> bool:
439
451
  """Stop a specific job by ID.
440
452
 
441
453
  Args:
442
454
  job_id: UUID of the job to stop.
455
+ timeout: Maximum seconds to wait for job to stop.
456
+
457
+ Returns:
458
+ True if job stopped cleanly, False if timed out or not found.
443
459
  """
444
- if job_id in self.jobs:
445
- job = self.jobs[job_id]
446
- # Unregister hooks before closing
447
- self.unregister_hooks(job_id)
448
- job.close_job()
449
- del self.jobs[job_id]
460
+ if job_id not in self.jobs:
461
+ return False
462
+
463
+ job = self.jobs[job_id]
464
+ # Unregister hooks before closing
465
+ self.unregister_hooks(job_id)
466
+ success = job.close_job(timeout=timeout)
467
+ del self.jobs[job_id]
468
+ return success
450
469
 
451
470
 
452
471
  def get_last_created_job(self):
@@ -461,10 +480,38 @@ class JobManager(object):
461
480
  raise ValueError(f"Job with ID {job_id} not found.")
462
481
 
463
482
 
464
- def detach_from_app(self):
465
- if self.process_session:
483
+ def detach_from_app(self, timeout: float = 3.0) -> bool:
484
+ """Detach from the current app session.
485
+
486
+ Args:
487
+ timeout: Maximum seconds to wait for detach.
488
+
489
+ Returns:
490
+ True if detached successfully, False if timed out or failed.
491
+ """
492
+ if not self.process_session:
493
+ return True
494
+
495
+ import concurrent.futures
496
+
497
+ def _detach():
466
498
  self.process_session.detach()
467
499
 
500
+ try:
501
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
502
+ future = executor.submit(_detach)
503
+ future.result(timeout=timeout)
504
+ self.process_session = None
505
+ return True
506
+ except concurrent.futures.TimeoutError:
507
+ self.logger.warning(f"Detach timed out after {timeout}s")
508
+ self.process_session = None # Clear anyway to avoid reuse
509
+ return False
510
+ except Exception as e:
511
+ self.logger.warning(f"Detach failed: {e}")
512
+ self.process_session = None
513
+ return False
514
+
468
515
 
469
516
  def stop_app(self, app_package):
470
517
  cmd = self._build_adb_command(["shell", "am", "force-stop", app_package])
@@ -670,23 +717,21 @@ class JobManager(object):
670
717
  """Get the current session mode ('spawn' or 'attach')."""
671
718
  return self._mode
672
719
 
673
- def reset_session(self) -> None:
720
+ def reset_session(self, timeout_per_job: float = 2.0, detach_timeout: float = 2.0) -> None:
674
721
  """Reset the session state for a new connection.
675
722
 
676
- Stops all jobs, clears hook registry, and resets session state.
723
+ Args:
724
+ timeout_per_job: Max seconds to wait per job when stopping.
725
+ detach_timeout: Max seconds to wait for session detach.
677
726
  """
678
- # Stop all running jobs
679
- self.stop_jobs()
727
+ # Stop all running jobs with timeout
728
+ self.stop_jobs(timeout_per_job=timeout_per_job)
680
729
 
681
730
  # Clear hook registry
682
731
  self._hook_registry.clear()
683
732
 
684
- # Detach from app
685
- if self.process_session:
686
- try:
687
- self.process_session.detach()
688
- except Exception:
689
- pass
733
+ # Detach from app with timeout
734
+ self.detach_from_app(timeout=detach_timeout)
690
735
 
691
736
  # Reset state
692
737
  self.process_session = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AndroidFridaManager
3
- Version: 1.9.4
3
+ Version: 1.9.6
4
4
  Summary: A python API in order to install and run the frida-server on an Android device.
5
5
  Home-page: https://github.com/fkie-cad/AndroidFridaManager
6
6
  Author: Daniel Baier
@@ -33,7 +33,7 @@ Dynamic: requires-dist
33
33
  Dynamic: requires-python
34
34
  Dynamic: summary
35
35
 
36
- ![version](https://img.shields.io/badge/version-1.9.4-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
36
+ ![version](https://img.shields.io/badge/version-1.9.6-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
37
37
 
38
38
  # AndroidFridaManager
39
39
 
@@ -0,0 +1,11 @@
1
+ AndroidFridaManager/FridaManager.py,sha256=pTcis4oLPBN5U7CdBqdUufwuG6QQDWTPBleTC7rtTbI,32145
2
+ AndroidFridaManager/__init__.py,sha256=T6AKtrGSLQ9M5bJoWDQcsRTJbSEbksdgrx3AAAdozRI,171
3
+ AndroidFridaManager/about.py,sha256=tytVSs7JHtb8wVUCXH89lg4vd5mn04MrHfzmOgihOUQ,98
4
+ AndroidFridaManager/job.py,sha256=jDDaqiG_ri-vAwwk045X80mzftd8INxh7UIv9YOELDU,10525
5
+ AndroidFridaManager/job_manager.py,sha256=Ce2lHLezRVKuj7r_RodH6Q8l43zL_cL5ZGhE1zGB-E8,27336
6
+ androidfridamanager-1.9.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
+ androidfridamanager-1.9.6.dist-info/METADATA,sha256=Dmw-7QAphEDeTviAvvTbHllZzksDSnO7lSfdgAF-3Xw,5141
8
+ androidfridamanager-1.9.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ androidfridamanager-1.9.6.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
10
+ androidfridamanager-1.9.6.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
11
+ androidfridamanager-1.9.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- AndroidFridaManager/FridaManager.py,sha256=pTcis4oLPBN5U7CdBqdUufwuG6QQDWTPBleTC7rtTbI,32145
2
- AndroidFridaManager/__init__.py,sha256=T6AKtrGSLQ9M5bJoWDQcsRTJbSEbksdgrx3AAAdozRI,171
3
- AndroidFridaManager/about.py,sha256=st5zrhGhtBpwZErvuVGrS-VQL9AmtAxFwL9AYgf-rqI,98
4
- AndroidFridaManager/job.py,sha256=sMkTk1rHdfNmh9jFNQuiL4oqsUEBCJcbP6p2DZQPbL0,6172
5
- AndroidFridaManager/job_manager.py,sha256=oPkzFK8XpwFhDHKuJmAoqylUDaV6VIKL_AikJBcg3iQ,25772
6
- androidfridamanager-1.9.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
- androidfridamanager-1.9.4.dist-info/METADATA,sha256=bghFYQPx8KRnUXKXZNyfHkPmBuTx3yBjT5_jXSZD9HM,5141
8
- androidfridamanager-1.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- androidfridamanager-1.9.4.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
10
- androidfridamanager-1.9.4.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
11
- androidfridamanager-1.9.4.dist-info/RECORD,,