Skip to content

Commit ba3e5b2

Browse files
authored
Merge branch 'develop' into color-allover
2 parents 04ef4e1 + 34e0bb9 commit ba3e5b2

File tree

6 files changed

+400
-476
lines changed

6 files changed

+400
-476
lines changed

src/classes/exceptions.py

+103-3
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,117 @@
2525
along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
2626
"""
2727

28+
import os
2829
import traceback
30+
import platform
31+
32+
from classes import info
2933
from classes.logger import log
30-
from classes.metrics import track_exception_stacktrace
34+
from classes.metrics import track_exception_stacktrace, track_metric_error
3135

3236

3337
def ExceptionHandler(exeception_type, exeception_value, exeception_traceback):
3438
"""Callback for any unhandled exceptions"""
35-
log.error('Unhandled Exception', exc_info=(exeception_type, exeception_value, exeception_traceback))
39+
log.error(
40+
'Unhandled Exception',
41+
exc_info=(exeception_type, exeception_value, exeception_traceback))
3642

3743
# Build string of stack trace
38-
stacktrace = "Python %s" % "".join(traceback.format_exception(exeception_type, exeception_value, exeception_traceback))
44+
stacktrace = "Python %s" % "".join(
45+
traceback.format_exception(
46+
exeception_type, exeception_value, exeception_traceback))
3947

4048
# Report traceback to webservice (if enabled)
4149
track_exception_stacktrace(stacktrace, "openshot-qt")
50+
51+
52+
def tail_file(f, n, offset=None):
53+
"""Read the end of a file (n number of lines)"""
54+
avg_line_length = 90
55+
to_read = n + (offset or 0)
56+
57+
while True:
58+
try:
59+
# Seek to byte position
60+
f.seek(-(avg_line_length * to_read), 2)
61+
except IOError:
62+
# Byte position not found
63+
f.seek(0)
64+
pos = f.tell()
65+
lines = f.read().splitlines()
66+
if len(lines) >= to_read or pos == 0:
67+
# Return the lines
68+
return lines[-to_read:offset and -offset or None]
69+
avg_line_length *= 2
70+
71+
72+
def libopenshot_crash_recovery():
73+
"""Walk libopenshot.log for the last line before this launch"""
74+
log_path = os.path.join(info.USER_PATH, "libopenshot.log")
75+
last_log_line = ""
76+
last_stack_trace = ""
77+
found_stack = False
78+
log_start_counter = 0
79+
if not os.path.exists(log_path):
80+
return
81+
with open(log_path, "rb") as f:
82+
# Read from bottom up
83+
for raw_line in reversed(tail_file(f, 500)):
84+
line = str(raw_line, 'utf-8')
85+
# Detect stack trace
86+
if "End of Stack Trace" in line:
87+
found_stack = True
88+
continue
89+
if "Unhandled Exception: Stack Trace" in line:
90+
found_stack = False
91+
continue
92+
if "libopenshot logging:" in line:
93+
log_start_counter += 1
94+
if log_start_counter > 1:
95+
# Found the previous log start, too old now
96+
break
97+
98+
if found_stack:
99+
# Append line to beginning of stacktrace
100+
last_stack_trace = line + last_stack_trace
101+
102+
# Ignore certain useless lines
103+
line.strip()
104+
if all(["---" not in line,
105+
"libopenshot logging:" not in line,
106+
not last_log_line,
107+
]):
108+
last_log_line = line
109+
110+
# Split last stack trace (if any)
111+
if last_stack_trace:
112+
# Get top line of stack trace (for metrics)
113+
last_log_line = last_stack_trace.split("\n")[0].strip()
114+
115+
# Send stacktrace for debugging (if send metrics is enabled)
116+
track_exception_stacktrace(last_stack_trace, "libopenshot")
117+
118+
# Clear / normalize log line (so we can roll them up in the analytics)
119+
if last_log_line:
120+
# Format last log line based on OS (since each OS can be formatted differently)
121+
if platform.system() == "Darwin":
122+
last_log_line = "mac-%s" % last_log_line[58:].strip()
123+
elif platform.system() == "Windows":
124+
last_log_line = "windows-%s" % last_log_line
125+
elif platform.system() == "Linux":
126+
last_log_line = "linux-%s" % last_log_line.replace("/usr/local/lib/", "")
127+
128+
# Remove '()' from line, and split. Trying to grab the beginning of the log line.
129+
last_log_line = last_log_line.replace("()", "")
130+
log_parts = last_log_line.split("(")
131+
if len(log_parts) == 2:
132+
last_log_line = "-%s" % log_parts[0].replace(
133+
"logger_libopenshot:INFO ", "").strip()[:64]
134+
elif len(log_parts) >= 3:
135+
last_log_line = "-%s (%s" % (log_parts[0].replace(
136+
"logger_libopenshot:INFO ", "").strip()[:64], log_parts[1])
137+
else:
138+
last_log_line = ""
139+
140+
# Report exception (with last libopenshot line... if found)
141+
track_metric_error("unhandled-crash%s" % last_log_line, True)

src/classes/style_tools.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
@file
3+
@brief Utility functions for manipulating SVG style attributes
4+
@author FeRD (Frank Dana) <[email protected]>
5+
6+
@section LICENSE
7+
8+
Copyright (c) 2008-2020 OpenShot Studios, LLC
9+
(http://www.openshotstudios.com). This file is part of
10+
OpenShot Video Editor (http://www.openshot.org), an open-source project
11+
dedicated to delivering high quality video editing and animation solutions
12+
to the world.
13+
14+
OpenShot Video Editor is free software: you can redistribute it and/or modify
15+
it under the terms of the GNU General Public License as published by
16+
the Free Software Foundation, either version 3 of the License, or
17+
(at your option) any later version.
18+
19+
OpenShot Video Editor is distributed in the hope that it will be useful,
20+
but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
GNU General Public License for more details.
23+
24+
You should have received a copy of the GNU General Public License
25+
along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
26+
"""
27+
28+
from classes.logger import log
29+
30+
31+
def style_to_dict(style: str) -> dict:
32+
"""Explode an SVG node style= attribute string into a dict representation"""
33+
styledict = {}
34+
try:
35+
# Fill the dict using key-value pairs produced by this comprehension
36+
styledict.update(
37+
# Make pairs of (property, value) by splitting on the first ':'
38+
(a.split(':', 1))
39+
# Using the list formed by splitting style on ';'
40+
for a in style.split(';')
41+
# Ignore any empty strings produced
42+
if a
43+
)
44+
return styledict
45+
except ValueError as ex:
46+
log.error(
47+
"style_to_dict failed to convert to dict: %s\n%s",
48+
ex, style)
49+
50+
51+
def dict_to_style(styledict: dict) -> str:
52+
"""Turn an exploded style dictionary back into a string"""
53+
# Glue the results of this comprehension together with ';' separators
54+
try:
55+
style = ";".join([
56+
# Produce a list of "key:value" strings
57+
":".join([k, v])
58+
# For every {key: value} in styledict
59+
for k, v in styledict.items()
60+
])
61+
# Don't forget the trailing semicolon!
62+
return style + ';'
63+
except ValueError as ex:
64+
import json
65+
log.error(
66+
"style_to_dict failed to generate string: %s\n%s",
67+
ex, json.dumps(styledict))
68+
69+
70+
def set_if_existing(d: dict, existing_key, new_value):
71+
if existing_key in d:
72+
d.update({existing_key: new_value})

src/windows/main_window.py

+10-113
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
"""
2929

3030
import os
31-
import platform
3231
import shutil
3332
import sys
3433
import webbrowser
@@ -50,18 +49,15 @@
5049
QLineEdit, QSlider, QLabel, QComboBox, QTextEdit
5150
)
5251

53-
from classes import info, ui_util, settings, qt_types, updates
52+
from classes import exceptions, info, settings, qt_types, ui_util, updates
5453
from classes.app import get_app
5554
from classes.conversion import zoomToSeconds, secondsToZoom
5655
from classes.exporters.edl import export_edl
5756
from classes.exporters.final_cut_pro import export_xml
5857
from classes.importers.edl import import_edl
5958
from classes.importers.final_cut_pro import import_xml
6059
from classes.logger import log
61-
from classes.metrics import (
62-
track_metric_session, track_metric_screen,
63-
track_metric_error, track_exception_stacktrace,
64-
)
60+
from classes.metrics import track_metric_session, track_metric_screen
6561
from classes.query import Clip, Transition, Marker, Track
6662
from classes.thumbnail import httpThumbnailServerThread
6763
from classes.time_parts import secondsToTimecode
@@ -214,101 +210,26 @@ def recover_backup(self):
214210
def create_lock_file(self):
215211
"""Create a lock file"""
216212
lock_path = os.path.join(info.USER_PATH, ".lock")
217-
lock_value = str(uuid4())
218-
219213
# Check if it already exists
220214
if os.path.exists(lock_path):
221-
# Walk the libopenshot log (if found), and try and find last line before this launch
222-
log_path = os.path.join(info.USER_PATH, "libopenshot.log")
223-
last_log_line = ""
224-
last_stack_trace = ""
225-
found_stack = False
226-
log_start_counter = 0
227-
if os.path.exists(log_path):
228-
with open(log_path, "rb") as f:
229-
# Read from bottom up
230-
for raw_line in reversed(self.tail_file(f, 500)):
231-
line = str(raw_line, 'utf-8')
232-
# Detect stack trace
233-
if "End of Stack Trace" in line:
234-
found_stack = True
235-
continue
236-
if "Unhandled Exception: Stack Trace" in line:
237-
found_stack = False
238-
continue
239-
if "libopenshot logging:" in line:
240-
log_start_counter += 1
241-
if log_start_counter > 1:
242-
# Found the previous log start, too old now
243-
break
244-
245-
if found_stack:
246-
# Append line to beginning of stacktrace
247-
last_stack_trace = line + last_stack_trace
248-
249-
# Ignore certain useless lines
250-
line.strip()
251-
if all(["---" not in line,
252-
"libopenshot logging:" not in line,
253-
not last_log_line,
254-
]):
255-
last_log_line = line
256-
257-
# Split last stack trace (if any)
258-
if last_stack_trace:
259-
# Get top line of stack trace (for metrics)
260-
last_log_line = last_stack_trace.split("\n")[0].strip()
261-
262-
# Send stacktrace for debugging (if send metrics is enabled)
263-
track_exception_stacktrace(last_stack_trace, "libopenshot")
264-
265-
# Clear / normalize log line (so we can roll them up in the analytics)
266-
if last_log_line:
267-
# Format last log line based on OS (since each OS can be formatted differently)
268-
if platform.system() == "Darwin":
269-
last_log_line = "mac-%s" % last_log_line[58:].strip()
270-
elif platform.system() == "Windows":
271-
last_log_line = "windows-%s" % last_log_line
272-
elif platform.system() == "Linux":
273-
last_log_line = "linux-%s" % last_log_line.replace("/usr/local/lib/", "")
274-
275-
# Remove '()' from line, and split. Trying to grab the beginning of the log line.
276-
last_log_line = last_log_line.replace("()", "")
277-
log_parts = last_log_line.split("(")
278-
if len(log_parts) == 2:
279-
last_log_line = "-%s" % log_parts[0].replace(
280-
"logger_libopenshot:INFO ", "").strip()[:64]
281-
elif len(log_parts) >= 3:
282-
last_log_line = "-%s (%s" % (log_parts[0].replace(
283-
"logger_libopenshot:INFO ", "").strip()[:64], log_parts[1])
284-
else:
285-
last_log_line = ""
286-
287-
# Throw exception (with last libopenshot line... if found)
288-
log.error(
289-
"Unhandled crash detected... will attempt to recover backup project: %s"
290-
% info.BACKUP_FILE)
291-
track_metric_error("unhandled-crash%s" % last_log_line, True)
292-
293-
# Remove file
215+
exceptions.libopenshot_crash_recovery()
216+
log.error("Unhandled crash detected. Preserving cache.")
294217
self.destroy_lock_file()
295-
296218
else:
297219
# Normal startup, clear thumbnails
298220
self.clear_all_thumbnails()
299221

300222
# Write lock file (try a few times if failure)
223+
lock_value = str(uuid4())
301224
for attempt in range(5):
302225
try:
303226
# Create lock file
304227
with open(lock_path, 'w') as f:
305228
f.write(lock_value)
306-
log.debug("Wrote value {} to lock file {}".format(
307-
lock_value, lock_path))
229+
log.debug("Wrote value %s to lock file %s", lock_value, lock_path)
308230
break
309231
except OSError:
310-
log.debug('Failed to write lock file (attempt: {})'.format(
311-
attempt), exc_info=1)
232+
log.debug("Failed to write lock file (attempt: %d)", attempt, exc_info=1)
312233
sleep(0.25)
313234

314235
def destroy_lock_file(self):
@@ -327,25 +248,6 @@ def destroy_lock_file(self):
327248
log.debug('Failed to destroy lock file (attempt: %s)' % attempt, exc_info=1)
328249
sleep(0.25)
329250

330-
def tail_file(self, f, n, offset=None):
331-
"""Read the end of a file (n number of lines)"""
332-
avg_line_length = 90
333-
to_read = n + (offset or 0)
334-
335-
while True:
336-
try:
337-
# Seek to byte position
338-
f.seek(-(avg_line_length * to_read), 2)
339-
except IOError:
340-
# Byte position not found
341-
f.seek(0)
342-
pos = f.tell()
343-
lines = f.read().splitlines()
344-
if len(lines) >= to_read or pos == 0:
345-
# Return the lines
346-
return lines[-to_read:offset and -offset or None]
347-
avg_line_length *= 2
348-
349251
def actionNew_trigger(self):
350252

351253
app = get_app()
@@ -413,14 +315,9 @@ def actionTitle_trigger(self):
413315
from windows.title_editor import TitleEditor
414316
win = TitleEditor()
415317
# Run the dialog event loop - blocking interaction on this window during that time
416-
result = win.exec_()
417-
if result == QDialog.Accepted:
418-
log.info('title editor add confirmed')
419-
else:
420-
log.info('title editor add cancelled')
318+
win.exec_()
421319

422320
def actionEditTitle_trigger(self):
423-
424321
# Loop through selected files (set 1 selected file if more than 1)
425322
for f in self.selected_files():
426323
if f.data.get("path").endswith(".svg"):
@@ -433,7 +330,7 @@ def actionEditTitle_trigger(self):
433330

434331
# show dialog for editing title
435332
from windows.title_editor import TitleEditor
436-
win = TitleEditor(file_path)
333+
win = TitleEditor(edit_file_path=file_path)
437334
# Run the dialog event loop - blocking interaction on this window during that time
438335
win.exec_()
439336

@@ -467,7 +364,7 @@ def actionDuplicateTitle_trigger(self):
467364

468365
# show dialog for editing title
469366
from windows.title_editor import TitleEditor
470-
win = TitleEditor(file_path, duplicate=True)
367+
win = TitleEditor(edit_file_path=file_path, duplicate=True)
471368
# Run the dialog event loop - blocking interaction on this window during that time
472369
return win.exec_()
473370

0 commit comments

Comments
 (0)