frida-fusion 0.1.16__tar.gz → 0.1.20__tar.gz

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.

Potentially problematic release.


This version of frida-fusion might be problematic. Click here for more details.

Files changed (46) hide show
  1. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/PKG-INFO +3 -1
  2. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/README.md +2 -0
  3. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/__meta__.py +2 -2
  4. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/config.py +16 -2
  5. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/fusion.py +118 -61
  6. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/libs/database.py +45 -21
  7. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/libs/helpers.js +154 -8
  8. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/libs/logger.py +40 -0
  9. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/module.py +73 -5
  10. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/crypto/crypto.py +9 -3
  11. frida_fusion-0.1.20/frida_fusion/modules/hermes_injector/hermes_hook.js +36 -0
  12. frida_fusion-0.1.20/frida_fusion/modules/hermes_injector/hermes_injector.js +191 -0
  13. frida_fusion-0.1.20/frida_fusion/modules/hermes_injector/hermes_injector.py +89 -0
  14. frida_fusion-0.1.20/frida_fusion/modules/okhttp_logging/okhttp-logging.js +1584 -0
  15. frida_fusion-0.1.20/frida_fusion/modules/okhttp_logging/okhttp-logging.py +134 -0
  16. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/reflection/reflection-stalker.py +1 -1
  17. frida_fusion-0.1.20/frida_fusion/modules/shared_preferences/__init__.py +0 -0
  18. frida_fusion-0.1.20/frida_fusion/modules/shared_preferences/shared_preferences.js +448 -0
  19. frida_fusion-0.1.20/frida_fusion/modules/shared_preferences/shared_preferences.py +179 -0
  20. frida_fusion-0.1.20/frida_fusion/modules/tls_unpinning/__init__.py +0 -0
  21. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion.egg-info/PKG-INFO +3 -1
  22. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion.egg-info/SOURCES.txt +9 -0
  23. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/LICENSE +0 -0
  24. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/__init__.py +0 -0
  25. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/__main__.py +0 -0
  26. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/args.py +0 -0
  27. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/exceptions.py +0 -0
  28. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/libs/__init__.py +0 -0
  29. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/libs/color.py +0 -0
  30. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/libs/scriptlocation.py +0 -0
  31. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/__init__.py +0 -0
  32. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/android_setings/__init__.py +0 -0
  33. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/android_setings/settings.js +0 -0
  34. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/android_setings/settings.py +0 -0
  35. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/crypto/__init__.py +0 -0
  36. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/crypto/crypto.js +0 -0
  37. {frida_fusion-0.1.16/frida_fusion/modules/tls_unpinning → frida_fusion-0.1.20/frida_fusion/modules/hermes_injector}/__init__.py +0 -0
  38. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/reflection/reflection-stalker.js +0 -0
  39. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion/modules/tls_unpinning/frida_multiple_unpinning.py +0 -0
  40. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion.egg-info/dependency_links.txt +0 -0
  41. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion.egg-info/entry_points.txt +0 -0
  42. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion.egg-info/requires.txt +0 -0
  43. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/frida_fusion.egg-info/top_level.txt +0 -0
  44. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/pyproject.toml +0 -0
  45. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/setup.cfg +0 -0
  46. {frida_fusion-0.1.16 → frida_fusion-0.1.20}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frida-fusion
3
- Version: 0.1.16
3
+ Version: 0.1.20
4
4
  Summary: Hook your mobile tests with Frida
5
5
  Author-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
6
6
  Maintainer-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
@@ -34,6 +34,7 @@ Requires-Dist: frida-tools>=10.8.0
34
34
  Dynamic: license-file
35
35
 
36
36
  # Frida Fusion
37
+ <img src="./fusion_logo.svg" alt="Frida Fusion logo" align="right" width="20%"/>
37
38
 
38
39
  Hook your mobile tests with Frida.
39
40
 
@@ -92,6 +93,7 @@ The Frida Fusion define/expose several functions to be used at frida scripts. Fo
92
93
  ```java
93
94
  # Send message/data to Frida-Fusion
94
95
  void fusion_sendMessage(String level, String message);
96
+ void fusion_sendError(Error error);
95
97
  void fusion_sendMessageWithTrace(String level, String message);
96
98
  void fusion_sendKeyValueData(String module, Object items);
97
99
 
@@ -1,4 +1,5 @@
1
1
  # Frida Fusion
2
+ <img src="./fusion_logo.svg" alt="Frida Fusion logo" align="right" width="20%"/>
2
3
 
3
4
  Hook your mobile tests with Frida.
4
5
 
@@ -57,6 +58,7 @@ The Frida Fusion define/expose several functions to be used at frida scripts. Fo
57
58
  ```java
58
59
  # Send message/data to Frida-Fusion
59
60
  void fusion_sendMessage(String level, String message);
61
+ void fusion_sendError(Error error);
60
62
  void fusion_sendMessageWithTrace(String level, String message);
61
63
  void fusion_sendKeyValueData(String module, Object items);
62
64
 
@@ -1,8 +1,8 @@
1
- __version__ = '0.1.16'
1
+ __version__ = '0.1.20'
2
2
  __title__ = "Frida Fusion"
3
3
  __description__ = "📱 frida-fusion - runtime mobile exploration"
4
4
  __url__ = "https://github.com/helviojunior/frida-fusion"
5
- __build__ = 0xfe136e5
5
+ __build__ = 0x472df92
6
6
  __author__ = "Helvio Junior (M4v3r1ck)"
7
7
  __author_email__ = "helvio_junior@hotmail.com"
8
8
  __license__ = "GPL-3.0"
@@ -7,7 +7,7 @@ import signal
7
7
  from argparse import Namespace
8
8
  from pathlib import Path
9
9
 
10
- from .module import Module, ModuleManager, InternalModule, ExternalModule
10
+ from .module import Module, ModuleManager, InternalModule, ExternalModule, LocalModule
11
11
  from .libs.color import Color
12
12
  from .libs.logger import Logger
13
13
  from .__meta__ import __version__
@@ -70,6 +70,8 @@ class Configuration(object):
70
70
 
71
71
  sys.argv[0] = 'frida-fusion'
72
72
 
73
+ Configuration.cmd_line = ' '.join([word for word in sys.argv])
74
+
73
75
  list_modules = any(['--list-modules' == word for word in sys.argv])
74
76
  #show_help = any(['-h' == word for word in sys.argv])
75
77
 
@@ -132,6 +134,8 @@ class Configuration(object):
132
134
  Color.pl('{!} {R}error: you must specify just one parameter {O}--package{R} or {O}--attach-pid{R}{W}\r\n')
133
135
  Configuration.mandatory()
134
136
 
137
+ Logger.pl(' {C}command line:{O} %s{W}' % Configuration.cmd_line)
138
+
135
139
  if args.app_id is not None:
136
140
  Configuration.package = args.app_id
137
141
  Logger.pl(' {C}package:{O} %s{W}' % Configuration.package)
@@ -184,9 +188,10 @@ class Configuration(object):
184
188
 
185
189
  Logger.pl(' {C}min debug level:{O} %s{W}' % str(args.debug_level).upper())
186
190
 
191
+ mods = ModuleManager.list_modules(local_path=Path(Configuration.frida_scripts))
187
192
  if (args.enabled_modules is not None and isinstance(args.enabled_modules, list)) or \
188
193
  (args.ignore_messages_modules is not None and isinstance(args.ignore_messages_modules, list)):
189
- mods = ModuleManager.list_modules()
194
+
190
195
  for mod in [
191
196
  m.strip()
192
197
  for md in args.enabled_modules
@@ -206,6 +211,7 @@ class Configuration(object):
206
211
  name = fm.safe_name()
207
212
  if name not in Configuration.enabled_modules.keys():
208
213
  Configuration.enabled_modules[name] = fm
214
+
209
215
  if args.ignore_messages_modules is not None and isinstance(args.ignore_messages_modules, list):
210
216
  for mod in [
211
217
  m.strip()
@@ -227,6 +233,14 @@ class Configuration(object):
227
233
  if name not in Configuration.ignore_messages_modules.keys():
228
234
  Configuration.ignore_messages_modules[name] = fm
229
235
 
236
+ # Enable user defined local modules
237
+ for _, fm in mods.items():
238
+ if isinstance(fm, LocalModule):
239
+ name = fm.safe_name()
240
+ if name not in Configuration.ignore_messages_modules.keys():
241
+ Configuration.ignore_messages_modules[name] = fm
242
+ Configuration.enabled_modules[name] = fm
243
+
230
244
  if len(Configuration.enabled_modules) > 0:
231
245
  Logger.pl(' {C}modules:{O} %s{W}' % ', '.join([
232
246
  m.name
@@ -35,6 +35,7 @@ class Fusion(object):
35
35
  print_timestamp = False
36
36
  max_filename = 28
37
37
 
38
+ _bundle_pattern = re.compile(r'(fusion_bundle\.js):(\d+)')
38
39
  _script_name = Path(__file__).name
39
40
  _db_jobs = queue.Queue()
40
41
 
@@ -83,9 +84,9 @@ class Fusion(object):
83
84
  elif Configuration.remote_host is not None:
84
85
  self.device = process.add_remote_device(Configuration.remote_host)
85
86
 
86
- except Exception as e:
87
+ except Exception as err:
87
88
  self.device = None
88
- Logger.pl('\n{!} {R}Error:{O} %s{W}' % str(e))
89
+ Logger.print_exception(err)
89
90
 
90
91
  return self.device
91
92
 
@@ -108,25 +109,6 @@ class Fusion(object):
108
109
  if v[0] <= loc.get_int_line() <= v[1]
109
110
  ]), loc)
110
111
 
111
- @classmethod
112
- def print_message(cls, level: str = "*", message: str = "",
113
- script_location: ScriptLocation = None):
114
-
115
- if Fusion.running is False and Logger.debug_level >= 2:
116
- return
117
-
118
- if script_location is None:
119
- script_location = ScriptLocation(
120
- file_name=Fusion._script_name
121
- )
122
-
123
- Logger.print_message(
124
- level=level,
125
- message=message,
126
- script_location=script_location,
127
- filename_col_len=Fusion.max_filename
128
- )
129
-
130
112
  def load_all_scripts(self):
131
113
  self.script_trace = {}
132
114
  offset = 1
@@ -162,7 +144,8 @@ class Fusion(object):
162
144
  src += dyn
163
145
 
164
146
  if os.path.isfile(Configuration.frida_scripts):
165
- files_js += [Configuration.frida_scripts]
147
+ if Path(Configuration.frida_scripts).suffix.lower() == ".js":
148
+ files_js += [Configuration.frida_scripts]
166
149
  else:
167
150
  files_js += [
168
151
  os.path.join(Configuration.frida_scripts, f)
@@ -170,7 +153,15 @@ class Fusion(object):
170
153
  if f.endswith(".js")
171
154
  ]
172
155
 
156
+ # Keep unique files
157
+ # Do not use list(set(files_js)) because it will lose the order of modules
158
+ done: set[str] = set()
173
159
  for file_path in files_js:
160
+ if file_path in done:
161
+ continue
162
+
163
+ done.add(file_path)
164
+
174
165
  file_name = Path(file_path).name
175
166
  file_data = self.sanitize_js(open(file_path, 'r', encoding='utf-8').read())
176
167
  if '#NOLOAD' in file_data:
@@ -190,7 +181,7 @@ class Fusion(object):
190
181
 
191
182
  line_cnt = len(file_data.split("\n")) - 1
192
183
 
193
- self.script_trace[file_name] = (offset, offset + line_cnt)
184
+ self.script_trace[file_name] = (offset, offset + line_cnt - 1)
194
185
  offset += line_cnt
195
186
 
196
187
  src += file_data
@@ -204,10 +195,17 @@ class Fusion(object):
204
195
  s = self.session.create_script(src, name="fusion_bundle")
205
196
  s.on("message", self.make_handler("fusion_bundle.js")) # register the message handler
206
197
  s.load()
207
- except Exception as e:
208
-
198
+ except Exception as err:
209
199
  try:
210
- err = str(e)
200
+ from traceback import format_exc
201
+ err_txt = 'Error:{O} %s{W}' % str(err)
202
+ err_txt += '\n{O}Full stack trace below\n'
203
+ err_txt += format_exc().strip()
204
+
205
+ err_txt = err_txt.replace('\n', '\n{W} ')
206
+ err_txt = err_txt.replace(' File', '{W}{D}File')
207
+ err_txt = err_txt.replace(' Exception: ', '{R}Exception: {O}')
208
+
211
209
  pattern = re.compile(r'script\(line (\d+)\):')
212
210
  matches = [
213
211
  (
@@ -217,15 +215,16 @@ class Fusion(object):
217
215
  line=m.group(1),
218
216
  ))
219
217
  )
220
- for m in pattern.finditer(err)
218
+ for m in pattern.finditer(err_txt)
221
219
  ]
222
220
  for m in matches:
223
- err = err.replace(m[0], f"{m[1].file_name}(line {m[1].line})")
224
- Logger.pl('{!} {R}Error:{O} %s{W}' % err)
221
+ err_txt = err_txt.replace(m[0], f"{m[1].file_name}(line {m[1].line})")
222
+
223
+ Logger.pl(err_txt)
225
224
  print("")
226
225
  sys.exit(1)
227
- except Exception:
228
- Logger.pl('{!} {R}Error:{O} %s{W}' % str(e))
226
+ except Exception as e2:
227
+ Logger.print_exception(e2)
229
228
  print("")
230
229
  sys.exit(1)
231
230
 
@@ -334,10 +333,11 @@ class Fusion(object):
334
333
  msg = base64.b64decode(msg).decode("UTF-8")
335
334
  except:
336
335
  pass
337
- self.print_message(mLevel, msg, script_location=script_location)
336
+
337
+ self.print_message_inst(mLevel, msg, script_location=script_location)
338
338
 
339
339
  elif mType == "key_value_data":
340
- self.print_message("V", "RAW JSON:\n %s" % (
340
+ self.print_message_inst("V", "RAW JSON:\n %s" % (
341
341
  json.dumps(jData, indent=4).replace("\n", "\n ")
342
342
  ), script_location=script_location)
343
343
 
@@ -367,7 +367,7 @@ class Fusion(object):
367
367
 
368
368
  # Legacy
369
369
  elif mType == "data":
370
- self.print_message("V", "RAW JSON:\n %s" % (
370
+ self.print_message_inst("V", "RAW JSON:\n %s" % (
371
371
  json.dumps(jData, indent=4).replace("\n", "\n ")
372
372
  ), script_location=script_location)
373
373
 
@@ -391,16 +391,16 @@ class Fusion(object):
391
391
 
392
392
  elif mType == "java-uncaught":
393
393
  self.insert_history('frida', json.dumps(jData))
394
- self.print_message("E", jData.get('stack', ''), script_location=script_location)
394
+ self.print_message_inst("E", jData.get('stack', ''), script_location=script_location)
395
395
 
396
396
  else:
397
- self.print_message(mLevel, message, script_location=script_location)
397
+ self.print_message_inst(mLevel, message, script_location=script_location)
398
398
 
399
399
  except SilentKillError as sk:
400
400
  skm = str(sk)
401
401
 
402
- self.print_message("D", "Silent kill requested",
403
- script_location=Logger.get_caller_info(stack_index=1))
402
+ self.print_message_inst("D", "Silent kill requested",
403
+ script_location=Logger.get_caller_info(stack_index=1))
404
404
  Fusion.running = False
405
405
  time.sleep(0.2)
406
406
  if skm != "":
@@ -410,8 +410,8 @@ class Fusion(object):
410
410
 
411
411
  except Exception as err:
412
412
  script_location = ScriptLocation(file_name=Fusion._script_name)
413
- self.print_message("E", message, script_location=script_location)
414
- self.print_message("E", payload, script_location=script_location)
413
+ self.print_message_inst("E", message, script_location=script_location)
414
+ self.print_message_inst("E", payload, script_location=script_location)
415
415
  self.print_exception(err)
416
416
 
417
417
  else:
@@ -425,7 +425,6 @@ class Fusion(object):
425
425
  stack = "Stack trace:\n"
426
426
  stack += message.get('stack', '')
427
427
 
428
- pattern = re.compile(r'(fusion_bundle\.js):(\d+)')
429
428
  matches = [
430
429
  (
431
430
  m.group(0),
@@ -434,7 +433,7 @@ class Fusion(object):
434
433
  line=m.group(2),
435
434
  ))
436
435
  )
437
- for m in pattern.finditer(stack)
436
+ for m in Fusion._bundle_pattern.finditer(stack)
438
437
  ]
439
438
  for m in matches:
440
439
  stack = stack.replace(m[0], f"{m[1].file_name}:{m[1].line}")
@@ -453,18 +452,19 @@ class Fusion(object):
453
452
  "stack": stack
454
453
  }))
455
454
 
456
- self.print_message("F", description + stack,
457
- script_location=script_location)
455
+ self.print_message_inst("F", description + stack,
456
+ script_location=script_location)
458
457
  Fusion.running = False
459
458
  time.sleep(0.2)
460
459
  Logger.pl('\n{+} {O}Exiting...{O}{W}')
461
460
  self.done.set()
462
461
  else:
463
- self.print_message("I", message, script_location=script_location)
464
- self.print_message("I", payload, script_location=script_location)
465
- except:
466
- self.print_message("I", message, script_location=script_location)
467
- self.print_message("I", payload, script_location=script_location)
462
+ self.print_message_inst("I", message, script_location=script_location)
463
+ self.print_message_inst("I", payload, script_location=script_location)
464
+ except Exception as e:
465
+ self.print_message_inst("I", message, script_location=script_location)
466
+ self.print_message_inst("I", payload, script_location=script_location)
467
+ self.print_exception(e)
468
468
 
469
469
  return handler
470
470
 
@@ -482,6 +482,26 @@ class Fusion(object):
482
482
  Logger.pl("")
483
483
  self.done.set()
484
484
 
485
+ def _replace_location(self, message: str) -> str:
486
+ try:
487
+ matches = [
488
+ (
489
+ m.group(0),
490
+ self.translate_location(dict(
491
+ file_name=m.group(1),
492
+ line=m.group(2),
493
+ ))
494
+ )
495
+ for m in Fusion._bundle_pattern.finditer(message)
496
+ ]
497
+ for m in matches:
498
+ message = message.replace(m[0], f"{m[1].file_name}:{m[1].line}")
499
+ except Exception as e:
500
+ print(e)
501
+ pass
502
+
503
+ return message
504
+
485
505
  def _raise_key_value_event(self,
486
506
  script_location: ScriptLocation = None,
487
507
  stack_trace: str = None,
@@ -497,11 +517,11 @@ class Fusion(object):
497
517
  )
498
518
  except SilentKillError as ske:
499
519
  raise ske
500
- except Exception as e:
501
- if Configuration.debug_level >= 2:
502
- self.print_message("E", f"Error resizing event to module {m.name}: {str(e)}")
503
- else:
504
- self.print_exception(e)
520
+ except Exception as err:
521
+ self.print_exception(
522
+ err,
523
+ script_location=Logger.get_error_info_from_format_exc(stack_index=-1)
524
+ )
505
525
 
506
526
  def _raise_data_event(self,
507
527
  script_location: ScriptLocation = None,
@@ -516,11 +536,48 @@ class Fusion(object):
516
536
  )
517
537
  except SilentKillError as ske:
518
538
  raise ske
519
- except Exception as e:
520
- if Configuration.debug_level >= 2:
521
- self.print_message("E", f"Error resizing event to module {m.name}: {str(e)}")
522
- else:
523
- self.print_exception(e)
539
+ except Exception as err:
540
+ self.print_exception(
541
+ err,
542
+ script_location=Logger.get_error_info_from_format_exc(stack_index=-1)
543
+ )
544
+
545
+ def print_message_inst(self, level: str = "*", message: str = "",
546
+ script_location: ScriptLocation = None):
547
+
548
+ return type(self)._print_message(
549
+ level=level,
550
+ message=self._replace_location(message),
551
+ script_location=script_location
552
+ )
553
+
554
+ @classmethod
555
+ def print_message(cls, level: str = "*", message: str = "",
556
+ script_location: ScriptLocation = None):
557
+ return cls._print_message(
558
+ level=level,
559
+ message=message,
560
+ script_location=script_location
561
+ )
562
+
563
+ @classmethod
564
+ def _print_message(cls, level: str = "*", message: str = "",
565
+ script_location: ScriptLocation = None):
566
+
567
+ if Fusion.running is False and Logger.debug_level >= 2:
568
+ return
569
+
570
+ if script_location is None:
571
+ script_location = ScriptLocation(
572
+ file_name=Fusion._script_name
573
+ )
574
+
575
+ Logger.print_message(
576
+ level=level,
577
+ message=message,
578
+ script_location=script_location,
579
+ filename_col_len=Fusion.max_filename
580
+ )
524
581
 
525
582
  @classmethod
526
583
  def insert_history(cls, source: str, data: str, stack_trace: str = ''):
@@ -539,7 +596,7 @@ class Fusion(object):
539
596
  db.insert_history(**kwargs)
540
597
 
541
598
  @classmethod
542
- def print_exception(cls, err):
599
+ def print_exception(cls, err, script_location: ScriptLocation = None):
543
600
  from traceback import format_exc
544
601
  err_txt = 'Error:{O} %s{W}' % str(err)
545
602
  err_txt += '\n{O}Full stack trace below\n'
@@ -553,7 +610,7 @@ class Fusion(object):
553
610
  level="E",
554
611
  message=Color.s(err_txt),
555
612
  filename_col_len=Fusion.max_filename,
556
- script_location=Logger.get_caller_info(stack_index=2)
613
+ script_location=script_location if script_location is not None else Logger.get_caller_info(stack_index=2)
557
614
  )
558
615
 
559
616
  @classmethod
@@ -4,6 +4,7 @@ import math
4
4
  import shutil
5
5
  import os.path
6
6
  import sqlite3
7
+ import time
7
8
  from functools import reduce
8
9
  from pathlib import Path
9
10
  from sqlite3 import Connection, ProgrammingError
@@ -78,7 +79,7 @@ class Database(object):
78
79
  (columns, values) = self.parse_args(kwargs)
79
80
  sql = "INSERT INTO {} ({}) VALUES ({})" \
80
81
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
81
- conn.execute(sql, values)
82
+ self.resilient_execute(conn, sql, values)
82
83
  conn.commit()
83
84
 
84
85
  @connect
@@ -89,7 +90,7 @@ class Database(object):
89
90
  (columns, values) = self.parse_args(kwargs)
90
91
  sql = "INSERT INTO {} ({}) VALUES ({})" \
91
92
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
92
- conn.execute(sql, values)
93
+ self.resilient_execute(conn, sql, values)
93
94
  conn.commit()
94
95
 
95
96
  @connect
@@ -98,7 +99,7 @@ class Database(object):
98
99
  (columns, values) = self.parse_args(kwargs)
99
100
  sql = "INSERT OR IGNORE INTO {} ({}) VALUES ({})" \
100
101
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
101
- conn.execute(sql, values)
102
+ self.resilient_execute(conn, sql, values)
102
103
  conn.commit()
103
104
 
104
105
  @connect
@@ -107,7 +108,7 @@ class Database(object):
107
108
  (columns, values) = self.parse_args(kwargs)
108
109
  sql = "INSERT OR REPLACE INTO {} ({}) VALUES ({})" \
109
110
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
110
- conn.execute(sql, values)
111
+ self.resilient_execute(conn, sql, values)
111
112
  conn.commit()
112
113
 
113
114
  def insert_update_one(self, table_name: str, **kwargs):
@@ -119,7 +120,7 @@ class Database(object):
119
120
  (columns, values) = self.parse_args(kwargs)
120
121
  sql = "INSERT OR IGNORE INTO {} ({}) VALUES ({})" \
121
122
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
122
- c = conn.execute(sql, values)
123
+ c = self.resilient_execute(conn, sql, values)
123
124
 
124
125
  status = {'inserted': c.rowcount, 'updated': 0}
125
126
 
@@ -135,7 +136,7 @@ class Database(object):
135
136
  sql += "{}".format(', '.join([f'{col} = ?' for col in u_columns]))
136
137
  if len(f_columns) > 0:
137
138
  sql += " WHERE {}".format(f' and '.join([f'{col} = ?' for col in f_columns]))
138
- c = conn.execute(sql, tuple(u_values + f_values, ))
139
+ c = self.resilient_execute(conn, sql, tuple(u_values + f_values, ))
139
140
  conn.commit()
140
141
 
141
142
  status['updated'] = c.rowcount
@@ -155,12 +156,16 @@ class Database(object):
155
156
  if len(columns) > 0:
156
157
  sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
157
158
 
158
- cursor = conn.execute(sql, values)
159
- if cursor.rowcount == 0:
160
- return []
159
+ data = []
160
+ with conn: # Transaction
161
+ cursor = self.resilient_execute(conn, sql, values)
162
+ if cursor.rowcount == 0:
163
+ return data
161
164
 
162
- columns = cursor.description
163
- return [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
165
+ columns = cursor.description
166
+ data = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
167
+
168
+ return data
164
169
 
165
170
  def select_first(self, table_name, **kwargs):
166
171
  data = self.select(table_name, **kwargs)
@@ -170,11 +175,15 @@ class Database(object):
170
175
 
171
176
  @connect
172
177
  def select_raw(self, conn: Connection, sql: str, args: any):
173
- cursor = conn.execute(sql, tuple(args,))
178
+ cursor = self.resilient_execute(conn, sql, tuple(args,))
174
179
  if cursor.rowcount == 0:
175
180
  return []
176
181
  columns = cursor.description
177
- return [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
182
+ data = []
183
+ with conn: # Transaction
184
+ data = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
185
+
186
+ return data
178
187
 
179
188
  @connect
180
189
  def select_count(self, conn: Connection, table_name, **kwargs) -> int:
@@ -187,10 +196,13 @@ class Database(object):
187
196
  sql = f"SELECT count(*) FROM {table_name}"
188
197
  if len(columns) > 0:
189
198
  sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
190
- cursor = conn.execute(sql, values)
191
- if cursor.rowcount == 0:
192
- return 0
193
- data = cursor.fetchone()
199
+
200
+ data = []
201
+ with conn: # Transaction
202
+ cursor = self.resilient_execute(conn, sql, values)
203
+ if cursor.rowcount == 0:
204
+ return 0
205
+ data = cursor.fetchone()
194
206
 
195
207
  return int(data[0])
196
208
 
@@ -205,7 +217,7 @@ class Database(object):
205
217
  sql = f"DELETE FROM {table_name}"
206
218
  if len(columns) > 0:
207
219
  sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
208
- conn.execute(sql, values)
220
+ self.resilient_execute(conn, sql, values)
209
221
  conn.commit()
210
222
 
211
223
  @connect
@@ -221,7 +233,7 @@ class Database(object):
221
233
  sql += "{}".format(', '.join([f'{col} = ?' for col in u_columns]))
222
234
  if len(f_columns) > 0:
223
235
  sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in f_columns]))
224
- conn.execute(sql, tuple(u_values + f_values, ))
236
+ self.resilient_execute(conn, sql, tuple(u_values + f_values, ))
225
237
  conn.commit()
226
238
 
227
239
  def get_constraints(self, conn: Connection) -> dict:
@@ -238,7 +250,7 @@ class Database(object):
238
250
  ' il.origin = "u" '
239
251
  'ORDER BY table_name, key_name, ii.seqno')
240
252
 
241
- cursor = conn.execute(sql)
253
+ cursor = self.resilient_execute(conn, sql)
242
254
  columns = cursor.description
243
255
  db_scheme = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
244
256
 
@@ -333,7 +345,7 @@ class Database(object):
333
345
 
334
346
  conn.commit()
335
347
 
336
- #Must get the constraints
348
+ # Must get the constraints
337
349
  self.get_constraints(conn)
338
350
 
339
351
  def insert_history(self, source: str, data: str, stack_trace: str = ''):
@@ -363,3 +375,15 @@ class Database(object):
363
375
  str
364
376
  """
365
377
  return ''.join(k for k in input_string if k.isalnum() or k in '_-')
378
+
379
+ @classmethod
380
+ def resilient_execute(cls, conn: Connection, *args, **kwargs):
381
+ for _ in range(5):
382
+ try:
383
+ return conn.execute(*args, **kwargs)
384
+ except sqlite3.OperationalError as e:
385
+ if 'database is locked' in str(e).lower():
386
+ time.sleep(0.3)
387
+ else:
388
+ raise e
389
+