Skip to content

Commit bec6070

Browse files
committed
fix: Improve state handling
1 parent 4ee40c7 commit bec6070

File tree

1 file changed

+71
-27
lines changed

1 file changed

+71
-27
lines changed

src/c2pa/c2pa.py

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,9 +1102,27 @@ def __exit__(self, exc_type, exc_val, exc_tb):
11021102
self.close()
11031103

11041104
def __del__(self):
1105-
"""Ensure resources are cleaned up if close() wasn't called."""
1106-
if hasattr(self, '_closed'):
1107-
self.close()
1105+
"""Ensure resources are cleaned up if close() wasn't called.
1106+
1107+
This destructor only cleans up if the object hasn't been explicitly closed.
1108+
"""
1109+
try:
1110+
# Only cleanup if not already closed and we have a valid stream
1111+
if hasattr(self, '_closed') and not self._closed:
1112+
if hasattr(self, '_stream') and self._stream:
1113+
# Use internal cleanup to avoid calling close() which could cause issues
1114+
try:
1115+
_lib.c2pa_release_stream(self._stream)
1116+
except Exception:
1117+
# Destructors shouldn't raise exceptions, just log silently
1118+
pass
1119+
finally:
1120+
self._stream = None
1121+
self._closed = True
1122+
self._initialized = False
1123+
except Exception:
1124+
# Destructors must not raise exceptions
1125+
pass
11081126

11091127
def close(self):
11101128
"""Release the stream resources.
@@ -1918,6 +1936,8 @@ def __init__(self, manifest_json: Any):
19181936
C2paError.Encoding: If manifest JSON contains invalid UTF-8 chars
19191937
C2paError.Json: If the manifest JSON cannot be serialized
19201938
"""
1939+
# Initialize state immediately to prevent race conditions
1940+
self._closed = False
19211941
self._builder = None
19221942

19231943
if not isinstance(manifest_json, str):
@@ -1981,6 +2001,8 @@ def from_archive(cls, stream: Any) -> 'Builder':
19812001
builder._builder = _lib.c2pa_builder_from_archive(stream_obj._stream)
19822002

19832003
if not builder._builder:
2004+
# Clean up the stream object if builder creation fails
2005+
stream_obj.close()
19842006
error = _parse_operation_result_for_error(_lib.c2pa_error())
19852007
if error:
19862008
raise C2paError(error)
@@ -1989,9 +2011,12 @@ def from_archive(cls, stream: Any) -> 'Builder':
19892011
return builder
19902012

19912013
def __del__(self):
1992-
"""Ensure resources are cleaned up if close() wasn't called."""
1993-
if hasattr(self, '_closed'):
1994-
self.close()
2014+
"""Ensure resources are cleaned up if close() wasn't called.
2015+
2016+
This destructor safely handles cleanup without causing double frees.
2017+
It only cleans up if the object hasn't been explicitly closed.
2018+
"""
2019+
self._cleanup_resources()
19952020

19962021
def close(self):
19972022
"""Release the builder resources.
@@ -2001,25 +2026,14 @@ def close(self):
20012026
Errors during cleanup are logged but not raised to ensure cleanup.
20022027
Multiple calls to close() are handled gracefully.
20032028
"""
2004-
# Track if we've already cleaned up
2005-
if not hasattr(self, '_closed'):
2006-
self._closed = False
2007-
20082029
if self._closed:
20092030
return
20102031

20112032
try:
2012-
# Clean up builder
2013-
if hasattr(self, '_builder') and self._builder:
2014-
try:
2015-
_lib.c2pa_builder_free(self._builder)
2016-
except Exception as e:
2017-
print(
2018-
Builder._ERROR_MESSAGES['builder_cleanup'].format(
2019-
str(e)), file=sys.stderr)
2020-
finally:
2021-
self._builder = None
2033+
# Use the internal cleanup method
2034+
self._cleanup_resources()
20222035
except Exception as e:
2036+
# Log any unexpected errors during close
20232037
print(
20242038
Builder._ERROR_MESSAGES['cleanup_error'].format(
20252039
str(e)), file=sys.stderr)
@@ -2039,9 +2053,7 @@ def set_no_embed(self):
20392053
into the asset when signing.
20402054
This is useful when creating cloud or sidecar manifests.
20412055
"""
2042-
if not self._builder:
2043-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
2044-
2056+
self._ensure_valid_state()
20452057
_lib.c2pa_builder_set_no_embed(self._builder)
20462058

20472059
def set_remote_url(self, remote_url: str):
@@ -2056,8 +2068,7 @@ def set_remote_url(self, remote_url: str):
20562068
Raises:
20572069
C2paError: If there was an error setting the remote URL
20582070
"""
2059-
if not self._builder:
2060-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
2071+
self._ensure_valid_state()
20612072

20622073
url_str = remote_url.encode('utf-8')
20632074
result = _lib.c2pa_builder_set_remote_url(self._builder, url_str)
@@ -2080,8 +2091,7 @@ def add_resource(self, uri: str, stream: Any):
20802091
Raises:
20812092
C2paError: If there was an error adding the resource
20822093
"""
2083-
if not self._builder:
2084-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
2094+
self._ensure_valid_state()
20852095

20862096
uri_str = uri.encode('utf-8')
20872097
with Stream(stream) as stream_obj:
@@ -2392,6 +2402,40 @@ def sign_file(self,
23922402
except Exception as e:
23932403
raise C2paError(f"Error signing file: {str(e)}") from e
23942404

2405+
def _ensure_valid_state(self):
2406+
"""Ensure the builder is in a valid state for operations.
2407+
2408+
Raises:
2409+
C2paError: If the builder is closed or invalid
2410+
"""
2411+
if self._closed:
2412+
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
2413+
if not self._builder:
2414+
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
2415+
2416+
def _cleanup_resources(self):
2417+
"""Internal cleanup method that safely releases native resources.
2418+
2419+
This method handles the actual cleanup logic and can be called
2420+
from both close() and __del__ without causing double frees.
2421+
"""
2422+
try:
2423+
# Only cleanup if not already closed and we have a valid builder
2424+
if hasattr(self, '_closed') and not self._closed:
2425+
if hasattr(self, '_builder') and self._builder and self._builder != 0:
2426+
try:
2427+
_lib.c2pa_builder_free(self._builder)
2428+
except Exception:
2429+
# Log cleanup errors but don't raise exceptions
2430+
pass
2431+
finally:
2432+
# Always clear the pointer and mark as closed
2433+
self._builder = None
2434+
self._closed = True
2435+
except Exception:
2436+
# Ensure we don't raise exceptions during cleanup
2437+
pass
2438+
23952439

23962440
def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]:
23972441
"""Convert a binary C2PA manifest into an embeddable version.

0 commit comments

Comments
 (0)