Title: Bulk export: unbounded memory growth (docs never closed) + crash on filenames with : (or other illegal chars) + hidden bodies silently skipped
Body:
I hit a few related issues running a bulk STEP export over a real project folder (~60 documents), all in commands/ExportCommand.py. Wrote small patches for each - sharing them here in case they're useful, happy to open a PR if you'd rather have that.
1. Memory grows unbounded during a bulk export
open_doc() opens and activates each document, but nothing ever closes it again. Fusion keeps every opened document fully loaded, so on a project with ~60 files this grew to 47GB of RAM by the end of the run.
Fix: close each document right after its export attempt, in a finally so it happens even on error/break:
def close_active_doc(wait_for_files=None, timeout=10.0):
"""Close the currently active document without saving.
ExportManager.execute() isn't documented/confirmed to be fully
synchronous for the on-disk write - closing the document immediately
after has been reported elsewhere to crash Fusion or leave a
truncated export. So when wait_for_files is given (the path(s) just
written), poll each file's size until it's stable across two checks
0.5s apart before closing, capped at `timeout` seconds so a failed
export can't hang the whole batch.
"""
if wait_for_files:
if isinstance(wait_for_files, str):
wait_for_files = [wait_for_files]
deadline = time.time() + timeout
for fp in wait_for_files:
last_size = -1
while time.time() < deadline:
try:
size = os.path.getsize(fp)
except OSError:
size = -1
if size >= 0 and size == last_size:
break
last_size = size
time.sleep(0.5)
app = adsk.core.Application.get()
doc = app.activeDocument
if doc is not None:
try:
doc.close(False)
except Exception:
pass
export_active_doc() needs to return the list of paths it wrote so the caller can pass them in:
def export_active_doc(folder, file_types, output_name):
...
written_files = []
for type_name, (export_function, extension) in solid_export_functions.items():
if is_type_selected(file_types, type_name):
export_name = folder + output_name + extension
export_name = dup_check(export_name)
export_options = export_function(export_name)
export_mgr.execute(export_options)
written_files.append(export_name) # <- add this in each branch (STEP/IGES/SAT/SMT, F3D, STL)
...
return written_files # <- add at the end
And in export_folder()'s f3d branch, wrap the export call in try/finally:
if file.fileExtension == "f3d":
open_doc(file)
written_files = []
try:
output_name = get_name(write_version, name_option)
written_files = export_active_doc(output_folder, file_types, output_name)
except ValueError as e:
ao.ui.messageBox(str(e))
except AttributeError as e:
ao.ui.messageBox(str(e))
break
finally:
close_active_doc(wait_for_files=written_files)
(Same finally: close_active_doc() in the electronics branch a bit further down - no written_files needed there since it exports one document to one product, or you can wire it through the same way if you want it as strict there too.)
2. Crash on documents with : (or other) illegal filename characters
A document named worm gear 30:1 crashed the whole batch partway through:
RuntimeError: 3 : The file name is not valid.
Neither get_name() nor get_electronics_name() sanitize the document name before it's used as a filename - any of \ / : * ? " < > | in a document name will hit this. Only found it because I happened to have a document named that way; anyone with similarly-named parts (measurements like 30:1, or names with /) will hit the exact same crash without warning.
Fix: sanitize at the source, so every caller is covered:
import re
def sanitize_filename(name):
return re.sub(r'[\\/:*?"<>|]', '-', name)
Then wrap both functions' return values:
def get_electronics_name(write_version):
...
return sanitize_filename(doc_name)
def get_name(write_version, option):
...
return sanitize_filename(output_name)
3. Hidden bodies/components are silently excluded from export
Fusion's own STEP export only includes visible geometry - a hidden body, or a body nested under a hidden parent occurrence, just doesn't show up in the output, with no warning. Confirmed this is Fusion's export behavior, not something Project-Archiver controls, but since a lot of people archive multi-config or WIP documents where some bodies are deliberately hidden, it's an easy way to end up with an incomplete export without knowing it.
Fix: an opt-in "Include hidden bodies/components?" checkbox (default off, so existing behavior is unchanged unless you turn it on). When enabled, walk the whole component tree before export and turn every light bulb on, then put back exactly what was off afterwards - isVisible/isLightBulbOn don't propagate up the tree on their own, so this has to happen at every level, not just the root:
def show_all_hidden(design):
"""Temporarily make every hidden body/occurrence visible for export.
Returns the list of objects that were actually OFF, so the caller can
restore exactly those afterwards and not touch anything that was
already visible on purpose.
"""
changed = []
def visit(component):
for body in component.bRepBodies:
if not body.isLightBulbOn:
changed.append(body)
body.isLightBulbOn = True
for occ in component.occurrences:
if not occ.isLightBulbOn:
changed.append(occ)
occ.isLightBulbOn = True
visit(occ.component)
visit(design.rootComponent)
return changed
def restore_hidden(changed):
"""Undo show_all_hidden() - hide exactly what was hidden before."""
for obj in changed:
try:
obj.isLightBulbOn = False
except Exception:
pass
Add the checkbox next to the existing "Preserve folder structure?" one:
include_hidden_input = inputs.addBoolValueInput('include_hidden_id', 'Include hidden bodies/components?', True, '', False)
include_hidden_input.isVisible = True
Read it in on_execute alongside folder_preserve, thread it through export_folder()'s signature (including its own recursive call) the same way folder_preserve already is, and in the f3d branch wrap the export with show/restore:
if include_hidden:
# ao is captured once at the top of export_folder(), before the loop -
# its .document/.design go stale the moment open_doc() switches the
# active document for the next file. Make a fresh AppObjects() here
# instead (it's a cheap wrapper, just reads app.activeDocument).
hidden_changed = show_all_hidden(AppObjects().design)
output_name = get_name(write_version, name_option)
written_files = export_active_doc(output_folder, file_types, output_name)
...
finally:
if hidden_changed:
restore_hidden(hidden_changed) # before closing, while this doc is still active
close_active_doc(wait_for_files=written_files)
Why I'm flagging all three together
All three came up in the same bulk export over the same project, and each led me straight to the next while poking at the same file. If it's easier to land these as separate PRs I'm happy to split them up, whatever works best for you.
Tested all three against a real ~60-document project: exports complete, memory stays flat, the 30:1-named document now exports as 30-1 without any error, and a document with a hidden body now exports it in full when the new checkbox is ticked (and exports as before when it's left unticked).
(One correction from my own first pass at #3: I originally called show_all_hidden(ao.design) reusing export_folder()'s own ao, captured once before the loop over all documents starts. That ao.document/ao.design go stale as soon as open_doc() switches the active document to the next file in the batch, so on anything past the very first document it was silently operating on the wrong document - no error, just no visible effect. Fixed by building a fresh AppObjects() right there instead, shown in the snippet above.)
Related existing issues
Looks like both of these have come up before and are still open: #19, #32 (filename sanitization) and #5, #33, #48 (documents not closing / memory). #35 links a fork (o-gent/Project-Archiver) that apparently already fixed both a while back, in case that's easier to pull in than reviewing another patch from scratch - either way, happy to help however's most useful.
Title: Bulk export: unbounded memory growth (docs never closed) + crash on filenames with
:(or other illegal chars) + hidden bodies silently skippedBody:
I hit a few related issues running a bulk STEP export over a real project folder (~60 documents), all in
commands/ExportCommand.py. Wrote small patches for each - sharing them here in case they're useful, happy to open a PR if you'd rather have that.1. Memory grows unbounded during a bulk export
open_doc()opens and activates each document, but nothing ever closes it again. Fusion keeps every opened document fully loaded, so on a project with ~60 files this grew to 47GB of RAM by the end of the run.Fix: close each document right after its export attempt, in a
finallyso it happens even on error/break:export_active_doc()needs to return the list of paths it wrote so the caller can pass them in:And in
export_folder()'s f3d branch, wrap the export call intry/finally:(Same
finally: close_active_doc()in the electronics branch a bit further down - nowritten_filesneeded there since it exports one document to one product, or you can wire it through the same way if you want it as strict there too.)2. Crash on documents with
:(or other) illegal filename charactersA document named
worm gear 30:1crashed the whole batch partway through:Neither
get_name()norget_electronics_name()sanitize the document name before it's used as a filename - any of\ / : * ? " < > |in a document name will hit this. Only found it because I happened to have a document named that way; anyone with similarly-named parts (measurements like30:1, or names with/) will hit the exact same crash without warning.Fix: sanitize at the source, so every caller is covered:
Then wrap both functions' return values:
3. Hidden bodies/components are silently excluded from export
Fusion's own STEP export only includes visible geometry - a hidden body, or a body nested under a hidden parent occurrence, just doesn't show up in the output, with no warning. Confirmed this is Fusion's export behavior, not something Project-Archiver controls, but since a lot of people archive multi-config or WIP documents where some bodies are deliberately hidden, it's an easy way to end up with an incomplete export without knowing it.
Fix: an opt-in "Include hidden bodies/components?" checkbox (default off, so existing behavior is unchanged unless you turn it on). When enabled, walk the whole component tree before export and turn every light bulb on, then put back exactly what was off afterwards -
isVisible/isLightBulbOndon't propagate up the tree on their own, so this has to happen at every level, not just the root:Add the checkbox next to the existing "Preserve folder structure?" one:
Read it in
on_executealongsidefolder_preserve, thread it throughexport_folder()'s signature (including its own recursive call) the same wayfolder_preservealready is, and in the f3d branch wrap the export with show/restore:Why I'm flagging all three together
All three came up in the same bulk export over the same project, and each led me straight to the next while poking at the same file. If it's easier to land these as separate PRs I'm happy to split them up, whatever works best for you.
Tested all three against a real ~60-document project: exports complete, memory stays flat, the
30:1-named document now exports as30-1without any error, and a document with a hidden body now exports it in full when the new checkbox is ticked (and exports as before when it's left unticked).(One correction from my own first pass at #3: I originally called
show_all_hidden(ao.design)reusingexport_folder()'s ownao, captured once before the loop over all documents starts. Thatao.document/ao.designgo stale as soon asopen_doc()switches the active document to the next file in the batch, so on anything past the very first document it was silently operating on the wrong document - no error, just no visible effect. Fixed by building a freshAppObjects()right there instead, shown in the snippet above.)Related existing issues
Looks like both of these have come up before and are still open: #19, #32 (filename sanitization) and #5, #33, #48 (documents not closing / memory). #35 links a fork (o-gent/Project-Archiver) that apparently already fixed both a while back, in case that's easier to pull in than reviewing another patch from scratch - either way, happy to help however's most useful.