Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ Since v0.2.0, Actions and Transitions export as `.st` rather than native xml. Th

Visualisations export as `<name>.vis.xml`. Earlier versions wrote `<name>.xml`, which silently collided with any POU of the same name (a `Main` program plus a `Main` visualisation is common). Old plain `.xml` exports still import correctly; re-exporting once migrates the tracked files.

## Export folder locked (Windows)

`Export To Files` writes the export into a sibling folder named `<project_name>.codescribe_staging` and only swaps it into place once the export completes. Earlier versions deleted the target folder up front, so a locked folder (an open Explorer window, IDE, git client or antivirus) aborted the export immediately and a mid-export crash left the on-disk copy destroyed.

If the target folder is locked and cannot be swapped, the staged files are synced into it instead and the export still succeeds. If that also fails, the error dialog reports the staging folder path; the completed export is preserved there, so nothing is lost.

## Project Templates

The intention of CODESCRIBE is not to export a complete copy of the project, but to only export the implementation logic of the project, enabling collaboration via git and other source control methods. An empty, but configured, underlying project file should also be committed to the repo to manage any other configuration that CODESYS provides (e.g. project level configuration, device configuration). For example, `Example Project_template_v1.project`:
Expand Down
9 changes: 4 additions & 5 deletions src/script_export_to_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from __future__ import print_function

import os
import shutil

import scriptengine # type: ignore

Expand Down Expand Up @@ -46,12 +45,10 @@ def export_child(child_obj, parent_obj, parent_folder_path):
src_folder = get_src_folder(scriptengine.projects.primary)
print("Writing to: " + src_folder)

if os.path.exists(src_folder):
shutil.rmtree(src_folder)
os.mkdir(src_folder)
staging_folder = begin_export_folder(src_folder)

for device_obj in get_device_entrypoints(scriptengine.projects.primary):
device_folder = os.path.join(src_folder, device_obj.get_name())
device_folder = os.path.join(staging_folder, device_obj.get_name())
os.mkdir(device_folder)

application = find_application(device_obj)
Expand All @@ -64,6 +61,8 @@ def export_child(child_obj, parent_obj, parent_folder_path):
communication = find_communication(device_obj)
if communication is not None:
export_communication(communication, device_folder)

finalize_export_folder(src_folder, staging_folder)
except Exception as e:
print(e)
ui_error_with_traceback("Export To Files failed!")
Expand Down
68 changes: 68 additions & 0 deletions src/util.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,81 @@
# REMEMBER: this is python 2.7
import io
import os
import shutil
import sys
import traceback

import scriptengine # type: ignore

from object_type import get_object_type

EXPORT_STAGING_SUFFIX = ".codescribe_staging"
EXPORT_BACKUP_SUFFIX = ".codescribe_backup"


class ExportFolderLockedError(EnvironmentError):
"""The target export folder could not be replaced. The completed export is
preserved in the staging folder."""


def begin_export_folder(target_folder):
# The export is written into a sibling staging folder and only swapped into
# place once it completes, so a locked target folder or a mid-export crash
# cannot destroy the existing on-disk copy.
staging_folder = target_folder + EXPORT_STAGING_SUFFIX
if os.path.exists(staging_folder):
shutil.rmtree(staging_folder)
os.mkdir(staging_folder)
return staging_folder


def _sync_export_files(staging_folder, target_folder):
# Overwrite sync only; files that exist in the target but not in staging are
# left in place, because deleting them would require the same folder access
# that already failed for the rename.
for dir_path, dir_names, file_names in os.walk(staging_folder):
relative = os.path.relpath(dir_path, staging_folder)
destination = target_folder if relative == "." else os.path.join(target_folder, relative)
if not os.path.isdir(destination):
os.makedirs(destination)
for file_name in file_names:
shutil.copy2(os.path.join(dir_path, file_name), os.path.join(destination, file_name))


def finalize_export_folder(target_folder, staging_folder):
backup_folder = target_folder + EXPORT_BACKUP_SUFFIX
try:
if os.path.exists(target_folder):
if os.path.exists(backup_folder):
shutil.rmtree(backup_folder)
os.rename(target_folder, backup_folder)
os.rename(staging_folder, target_folder)
except (OSError, IOError) as rename_error:
# On Windows these renames fail while another program holds a handle on
# the target folder. Fall back to copying the staged files into it.
try:
_sync_export_files(staging_folder, target_folder)
shutil.rmtree(staging_folder)
except (OSError, IOError):
raise ExportFolderLockedError(
"Could not replace the export folder "
+ target_folder
+ ". It is likely locked by another program (an open Explorer window, IDE, git client or antivirus)."
+ " The completed export is preserved in "
+ staging_folder
+ ". Original error: "
+ str(rename_error)
)
print("Export folder " + target_folder + " is in use; synced the staged files into it instead of swapping folders.")
return
try:
if os.path.exists(backup_folder):
shutil.rmtree(backup_folder)
except (OSError, IOError) as backup_error:
# The export itself succeeded at this point; a leftover backup folder is
# not worth failing it over.
print("Warning: could not delete backup folder " + backup_folder + ": " + str(backup_error))


def ui_info(message):
# Blocking dialog; the message view is easy to miss, so entry scripts report
Expand Down
Loading