@tsplc/build 0.0.0-stage → 0.2.0
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.
- package/LICENSE +21 -0
- package/README.md +6 -2
- package/dist/cli.js +1696 -0
- package/dist/cli.js.map +34 -0
- package/dist/index.js +1683 -0
- package/dist/index.js.map +31 -0
- package/dist/libraries.js +128 -0
- package/dist/libraries.js.map +10 -0
- package/docker/Dockerfile +173 -0
- package/docker/Dockerfile.dockerignore +18 -0
- package/docker/Dockerfile.source +15 -0
- package/docker/build-image +38 -0
- package/docker/codesys-build +5 -0
- package/docker/config.ts +8 -0
- package/docker/control-linux-sl/Dockerfile +38 -0
- package/docker/edge-gateway/Dockerfile +17 -0
- package/docker/entrypoint.sh +10 -0
- package/docker/install/NOTICE.md +21 -0
- package/docker/install/core.sh +36 -0
- package/docker/install/decode.c +12 -0
- package/docker/install/device/Runtime.ico +0 -0
- package/docker/install/device/Runtime.png +0 -0
- package/docker/install/device/codesyscontrol.devdesc.xml +1502 -0
- package/docker/install/extract.py +43 -0
- package/docker/install/helper-launcher.c +46 -0
- package/docker/install/install.sh +94 -0
- package/docker/install/prerequisites.sh +15 -0
- package/docker/install/verify.py +133 -0
- package/docker/install/verify.sh +34 -0
- package/docker/install/wine-ncrypt/COPYING.LIB +502 -0
- package/docker/install/wine-ncrypt/README.md +28 -0
- package/docker/install/wine-ncrypt/enable-oaep.py +34 -0
- package/docker/install/wine-ncrypt/main.c +619 -0
- package/docker/install/wine-ncrypt/oaep.patch +11 -0
- package/docker/install/wine-signal/README.md +22 -0
- package/docker/install/wine-signal/build.sh +10 -0
- package/docker/install/wine-signal/check.sh +34 -0
- package/docker/install/wine-signal/noop-i386.c +2 -0
- package/docker/install/wine-signal/signal-state.c +105 -0
- package/docker/install/wine-signal/tests/avx-registers.c +53 -0
- package/docker/install/wine-signal/tests/frame-and-handler.c +192 -0
- package/docker/install/wine-signal/tests/loader-i386.c +8 -0
- package/docker/install/wine-signal/tests/signal-return.c +47 -0
- package/docker/install/wine-signal/wine +4 -0
- package/docker/runner.ts +156 -0
- package/package.json +53 -4
- package/python/build.py +660 -0
- package/python/codesys_login.py +100 -0
- package/python/codesys_online.py +327 -0
- package/python/test_build.py +444 -0
- package/python/test_codesys_login.py +120 -0
- package/python/test_codesys_online.py +184 -0
- package/python/test_installer_packages.py +216 -0
- package/python/test_installer_verification.py +307 -0
- package/python/test_worker.py +142 -0
- package/python/worker.py +102 -0
- package/runtime/install.sh +74 -0
- package/src/__tests__/fixtures/xml/fb-simple.xml +40 -0
- package/src/__tests__/fixtures/xml/fb-state-machine.xml +86 -0
- package/src/__tests__/fixtures/xml/prg-cyclic.xml +44 -0
- package/src/artifacts.ts +77 -0
- package/src/build.ts +87 -0
- package/src/cli.ts +116 -0
- package/src/config.ts +12 -0
- package/src/controller-online.ts +297 -0
- package/src/controller-session.ts +298 -0
- package/src/diagnostics.ts +68 -0
- package/src/docker-build.ts +163 -0
- package/src/docker-context.ts +35 -0
- package/src/docker-run.ts +103 -0
- package/src/host/codesys-command.ts +45 -0
- package/src/host/codesys-host.ts +145 -0
- package/src/host/codesys-worker.ts +348 -0
- package/src/host/windows-path.ts +73 -0
- package/src/index.ts +43 -0
- package/src/io.ts +127 -0
- package/src/libraries.ts +166 -0
- package/src/package-path.ts +5 -0
- package/src/run.ts +133 -0
- package/src/runtime-status.ts +21 -0
- package/src/sources.ts +248 -0
- package/src/st/declarations.ts +229 -0
- package/src/st/lexer.ts +125 -0
- package/src/st/parse.ts +202 -0
- package/src/st/reader.ts +50 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""A long-lived login to the downloaded application; executed by CODESYS's IronPython engine."""
|
|
2
|
+
from __future__ import print_function
|
|
3
|
+
|
|
4
|
+
from codesys_online import (connect_controller, controller_ids, controller_state, core_application,
|
|
5
|
+
load_downloaded_reference, session_login)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def open_login(engine, request):
|
|
9
|
+
"""Log in to the controller's running application without changing it.
|
|
10
|
+
|
|
11
|
+
The project is a copy of the downloaded baseline, so the login never offers
|
|
12
|
+
a download: the controller and the compiler reference must both match it.
|
|
13
|
+
"""
|
|
14
|
+
baseline = request['baseline']
|
|
15
|
+
project = engine.projects.open(request['project'], primary=True)
|
|
16
|
+
online_app = None
|
|
17
|
+
try:
|
|
18
|
+
application = project.active_application
|
|
19
|
+
if application is None or str(application.guid) != baseline['applicationGuid']:
|
|
20
|
+
raise RuntimeError('The saved project does not contain the downloaded application')
|
|
21
|
+
connect_controller(engine, application, request['online'])
|
|
22
|
+
online_app = engine.online.create_online_application(application)
|
|
23
|
+
core = core_application(online_app)
|
|
24
|
+
session_login(core)
|
|
25
|
+
try:
|
|
26
|
+
if controller_ids(core) != baseline['controllerIds']:
|
|
27
|
+
raise RuntimeError('The controller runs a different application than the last deployment')
|
|
28
|
+
if load_downloaded_reference(application) != baseline['controllerIds']:
|
|
29
|
+
raise RuntimeError('Downloaded compiler reference differs from the controller')
|
|
30
|
+
finally:
|
|
31
|
+
core.Logout()
|
|
32
|
+
# The core login, unlike the scripting wrapper's, never creates or downloads an application.
|
|
33
|
+
if not core.Login(True):
|
|
34
|
+
raise RuntimeError('Controller application disappeared before login')
|
|
35
|
+
except Exception:
|
|
36
|
+
if online_app is not None:
|
|
37
|
+
online_app.Dispose()
|
|
38
|
+
project.close()
|
|
39
|
+
raise
|
|
40
|
+
return {'project': project, 'online': online_app, 'core': core}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def apply_prepared(online_app, prepared, operation):
|
|
44
|
+
try:
|
|
45
|
+
operation()
|
|
46
|
+
except Exception as error:
|
|
47
|
+
failed = getattr(error, 'failed_expressions', None)
|
|
48
|
+
if failed is None:
|
|
49
|
+
raise
|
|
50
|
+
raise RuntimeError('The controller rejected ' + ', '.join(str(expression) for expression in failed))
|
|
51
|
+
finally:
|
|
52
|
+
# An unapplied preparation would otherwise ride along with the next write or force.
|
|
53
|
+
for expression in prepared:
|
|
54
|
+
online_app.set_prepared_value(expression, None)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_command(engine, login, command):
|
|
58
|
+
online_app = login['online']
|
|
59
|
+
name = command['command']
|
|
60
|
+
if name == 'read':
|
|
61
|
+
return {'values': {expression: online_app.read_value(expression) for expression in command['expressions']}}
|
|
62
|
+
if name in ('write', 'force'):
|
|
63
|
+
values = command['values']
|
|
64
|
+
if name == 'write':
|
|
65
|
+
# The controller rejects writing a forced variable with only a generic error.
|
|
66
|
+
forced = set(str(expression).lower() for expression in online_app.get_forced_expressions())
|
|
67
|
+
blocked = [expression for expression in values if expression.lower() in forced]
|
|
68
|
+
if blocked:
|
|
69
|
+
raise RuntimeError(', '.join(blocked) + ' is forced; unforce it before writing')
|
|
70
|
+
for expression, value in values.items():
|
|
71
|
+
online_app.set_prepared_value(expression, value)
|
|
72
|
+
apply_prepared(online_app, values.keys(),
|
|
73
|
+
online_app.write_prepared_values if name == 'write' else online_app.force_prepared_values)
|
|
74
|
+
elif name == 'unforce':
|
|
75
|
+
expressions = command['expressions']
|
|
76
|
+
if expressions:
|
|
77
|
+
for expression in expressions:
|
|
78
|
+
online_app.set_unforce_value(expression, command['restore'])
|
|
79
|
+
# CODESYS applies prepared unforces with its force command.
|
|
80
|
+
apply_prepared(online_app, expressions, online_app.force_prepared_values)
|
|
81
|
+
else:
|
|
82
|
+
apply_prepared(online_app, [], online_app.unforce_all_values)
|
|
83
|
+
elif name == 'start':
|
|
84
|
+
online_app.start()
|
|
85
|
+
elif name == 'stop':
|
|
86
|
+
online_app.stop()
|
|
87
|
+
elif name == 'reset':
|
|
88
|
+
online_app.reset(getattr(engine.ResetOption, command['kind']))
|
|
89
|
+
elif name != 'state':
|
|
90
|
+
raise ValueError('Unknown login command: ' + name)
|
|
91
|
+
return {'state': controller_state(login['core']),
|
|
92
|
+
'forced': [str(expression) for expression in online_app.get_forced_expressions()]}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def close_login(login):
|
|
96
|
+
try:
|
|
97
|
+
login['online'].logout()
|
|
98
|
+
login['online'].Dispose()
|
|
99
|
+
finally:
|
|
100
|
+
login['project'].close()
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""CODESYS 3.5.22.30 online operations; executed by its IronPython engine."""
|
|
2
|
+
from __future__ import print_function
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from build import CompilerError, apply_controller, fail_on_errors, import_objects, reported_errors, unique_child
|
|
10
|
+
from System import Activator, AppDomain, Array, Enum, Environment, Object
|
|
11
|
+
from System.Net import Dns, IPAddress
|
|
12
|
+
from System.Reflection import BindingFlags, TargetInvocationException
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def core_application(online_application):
|
|
16
|
+
# The shipped scripting wrapper omits Keep from its login switch. Its core
|
|
17
|
+
# application provides a session-only login, which never compiles/downloads.
|
|
18
|
+
# This narrow bridge is pinned to the scripting version in our Docker image.
|
|
19
|
+
kind = online_application.GetType()
|
|
20
|
+
if kind.FullName != '_3S.CoDeSys.ScriptDriverOnline.OnlineApplication' or str(kind.Assembly.GetName().Version) != '4.2.0.0':
|
|
21
|
+
raise RuntimeError('Unsupported CODESYS online scripting implementation')
|
|
22
|
+
prop = kind.GetProperty('Application', BindingFlags.Instance | BindingFlags.NonPublic)
|
|
23
|
+
if prop is None:
|
|
24
|
+
raise RuntimeError('CODESYS session-only application API is unavailable')
|
|
25
|
+
return prop.GetValue(online_application, None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def loaded_type(name):
|
|
29
|
+
for assembly in AppDomain.CurrentDomain.GetAssemblies():
|
|
30
|
+
kind = assembly.GetType(name, False)
|
|
31
|
+
if kind is not None:
|
|
32
|
+
return kind
|
|
33
|
+
raise RuntimeError('CODESYS API type is unavailable: ' + name)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def session_login(core):
|
|
37
|
+
kind = loaded_type('_3S.CoDeSys.OnlineManager.OnlineApplicationLoginConfiguration')
|
|
38
|
+
config = Activator.CreateInstance(kind)
|
|
39
|
+
mode = loaded_type('_3S.CoDeSys.Core.Online.ApplicationLoginClientModes')
|
|
40
|
+
kind.GetProperty('ClientModes').SetValue(config, Enum.Parse(mode, 'ReadOnly'), None)
|
|
41
|
+
api = loaded_type('_3S.CoDeSys.Core.Online.IOnlineApplication23')
|
|
42
|
+
login = api.GetMethod('Login')
|
|
43
|
+
if not login.Invoke(core, Array[Object]([True, config])):
|
|
44
|
+
raise RuntimeError('The controller does not contain this application')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def controller_ids(core):
|
|
48
|
+
ids = {'codeId': str(core.CodeId), 'dataId': str(core.DataId)}
|
|
49
|
+
if any(value == '00000000-0000-0000-0000-000000000000' for value in ids.values()):
|
|
50
|
+
raise RuntimeError('Controller application has no downloaded code/data identity')
|
|
51
|
+
return ids
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def controller_state(core):
|
|
55
|
+
# Login updates IDs synchronously, but ApplicationState is timer-owned.
|
|
56
|
+
# Query the original device's application-list service for a fresh state.
|
|
57
|
+
name_property = core.GetType().GetProperty('ApplicationName', BindingFlags.Instance | BindingFlags.NonPublic)
|
|
58
|
+
name = str(name_property.GetValue(core, None))
|
|
59
|
+
api = loaded_type('_3S.CoDeSys.Core.Online.IOnlineDevice6')
|
|
60
|
+
arguments = Array[Object]([None, None])
|
|
61
|
+
api.GetMethod('ReadApplicationList').Invoke(core.OnlineDevice, arguments)
|
|
62
|
+
names, states = arguments[0], arguments[1]
|
|
63
|
+
if names is None or states is None or len(names) != len(states):
|
|
64
|
+
raise RuntimeError('Controller returned an incomplete application-state list')
|
|
65
|
+
found = [str(states[index]) for index in range(len(names)) if str(names[index]) == name]
|
|
66
|
+
if len(found) != 1 or found[0] == 'unknown':
|
|
67
|
+
raise RuntimeError('Controller application state is unavailable: ' + name)
|
|
68
|
+
return found[0]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def require_current_boot_project(core):
|
|
72
|
+
arguments = Array[Object]([False, False])
|
|
73
|
+
api = loaded_type('_3S.CoDeSys.Core.Online.IOnlineApplication8')
|
|
74
|
+
api.GetMethod('CheckFileConsistency').Invoke(core, arguments)
|
|
75
|
+
if not arguments[0]:
|
|
76
|
+
raise RuntimeError('Controller boot project does not match the downloaded application')
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def create_controller_boot_project(core):
|
|
80
|
+
# Reflection retains the CLR stack IronPython otherwise maps to ValueError.
|
|
81
|
+
api = loaded_type('_3S.CoDeSys.Core.Online.IOnlineApplication3')
|
|
82
|
+
try:
|
|
83
|
+
api.GetMethod('CreateBootProject').Invoke(core, None)
|
|
84
|
+
except TargetInvocationException as error:
|
|
85
|
+
cause = error.InnerException if error.InnerException is not None else error
|
|
86
|
+
raise RuntimeError('Controller boot project creation failed:\n' + cause.ToString())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def generate_online_code(application):
|
|
90
|
+
environment = loaded_type('_3S.CoDeSys.ScriptDriverOnline.APEnvironment')
|
|
91
|
+
prop = environment.GetProperty('LanguageModelMgr', BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
|
|
92
|
+
manager = prop.GetValue(None, None)
|
|
93
|
+
if not manager.GenerateCode(application.guid, True, False):
|
|
94
|
+
raise RuntimeError('CODESYS cannot generate an online change for this source')
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def consolidated_manager():
|
|
98
|
+
kind = loaded_type('_3S.CoDeSys.LanguageModelManager.LanguageModelManagerConsolidated')
|
|
99
|
+
return kind.GetProperty('_LMM', BindingFlags.Static | BindingFlags.NonPublic).GetValue(None, None)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def context_ids(context):
|
|
103
|
+
if context is None:
|
|
104
|
+
raise RuntimeError('Compiler context is unavailable')
|
|
105
|
+
ids = {'codeId': str(context.CodeId), 'dataId': str(context.DataId)}
|
|
106
|
+
if any(value == '00000000-0000-0000-0000-000000000000' for value in ids.values()):
|
|
107
|
+
raise RuntimeError('Compiler context has no generated code/data identity')
|
|
108
|
+
return ids
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def load_downloaded_reference(application):
|
|
112
|
+
# Background reference loading may not have populated the language model
|
|
113
|
+
# when a new headless worker receives its first operation. This original
|
|
114
|
+
# synchronous path reads the candidate's saved compileinfo without building.
|
|
115
|
+
context = consolidated_manager().GetReferenceContextSynchronLoad(application.guid)
|
|
116
|
+
if context is None:
|
|
117
|
+
raise RuntimeError('Downloaded compiler reference could not be loaded')
|
|
118
|
+
return context_ids(context)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def remove_deleted(parent, previous, current):
|
|
122
|
+
"""Reconcile only source-owned objects; preserve GUIDs of unchanged objects."""
|
|
123
|
+
new = {item['name'].lower(): item for item in current}
|
|
124
|
+
for old in previous:
|
|
125
|
+
obj = unique_child(parent, old['name'])
|
|
126
|
+
fresh = new.get(old['name'].lower())
|
|
127
|
+
if obj is None:
|
|
128
|
+
raise RuntimeError('Saved source object is missing: ' + old['name'])
|
|
129
|
+
if fresh is None or old['kind'] != fresh['kind'] or bool(old.get('persistent')) != bool(fresh.get('persistent')):
|
|
130
|
+
obj.remove()
|
|
131
|
+
else:
|
|
132
|
+
remove_deleted(obj, old.get('children', []), fresh.get('children', []))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def reconcile(application, previous, current):
|
|
136
|
+
# A folder move changes placement rather than text. Remove/recreate just
|
|
137
|
+
# that moved object, letting the vendor decide whether the update is valid.
|
|
138
|
+
new = {(tuple(item['folder']), item['name'].lower()): item for item in current}
|
|
139
|
+
for old in previous:
|
|
140
|
+
parent = application
|
|
141
|
+
for name in old['folder']:
|
|
142
|
+
parent = unique_child(parent, name)
|
|
143
|
+
if parent is None:
|
|
144
|
+
raise RuntimeError('Saved source folder is missing: ' + name)
|
|
145
|
+
fresh = new.get((tuple(old['folder']), old['name'].lower()))
|
|
146
|
+
remove_deleted(parent, [old], [fresh] if fresh else [])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def fail_on_errors_after_transfer(engine, progress):
|
|
150
|
+
"""Like fail_on_errors, once the controller runs the verified application.
|
|
151
|
+
|
|
152
|
+
Logging in to a Linux SL controller loads its device's visualization placeholders, which
|
|
153
|
+
nothing in a tsPLC project resolves. The application compiled without them, so the
|
|
154
|
+
engineering session reports them rather than failing a completed transfer.
|
|
155
|
+
"""
|
|
156
|
+
errors, unresolved = [], []
|
|
157
|
+
for error in reported_errors(engine):
|
|
158
|
+
placeholder = re.match(r"Could not open library '(#[^']+)'.*could not be resolved", error['text'])
|
|
159
|
+
if placeholder:
|
|
160
|
+
unresolved.append(placeholder.group(1))
|
|
161
|
+
else:
|
|
162
|
+
errors.append(error)
|
|
163
|
+
if unresolved:
|
|
164
|
+
progress('Ignoring library placeholders the login loaded but nothing resolves: ' + ', '.join(unresolved))
|
|
165
|
+
if errors:
|
|
166
|
+
raise CompilerError(errors)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def connect_controller(engine, application, settings):
|
|
170
|
+
"""Route the application's controller through the configured gateway; returns it and its credentials."""
|
|
171
|
+
device = application.parent
|
|
172
|
+
while device is not None and not getattr(device, 'is_device', False):
|
|
173
|
+
device = device.parent
|
|
174
|
+
if device is None:
|
|
175
|
+
raise RuntimeError('Application has no controller device')
|
|
176
|
+
gateway_ip = str(Dns.GetHostAddresses(settings['gateway']['host'])[0])
|
|
177
|
+
device_ip = str(Dns.GetHostAddresses(settings['device']['host'])[0])
|
|
178
|
+
gateway = engine.online.gateways.add_new_gateway('codesys-build', {0: gateway_ip, 1: settings['gateway']['port']})
|
|
179
|
+
device.set_gateway_and_ip_address(gateway.guid, IPAddress.Parse(device_ip), settings['device']['port'])
|
|
180
|
+
credentials = settings.get('credentials')
|
|
181
|
+
if not credentials:
|
|
182
|
+
return device, None
|
|
183
|
+
username, password = (os.environ.get(credentials[key]) for key in ('usernameEnv', 'passwordEnv'))
|
|
184
|
+
if not username or not password:
|
|
185
|
+
raise RuntimeError('CODESYS online credential environment variables are required')
|
|
186
|
+
engine.online.set_specific_credentials(device, username, password)
|
|
187
|
+
return device, (username, password)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def run_online(engine, request):
|
|
191
|
+
progress_path = request.get('progress')
|
|
192
|
+
def progress(stage):
|
|
193
|
+
if progress_path:
|
|
194
|
+
with io.open(progress_path, 'a', encoding='utf-8') as output:
|
|
195
|
+
output.write(u'{0}Z {1}\n'.format(time.strftime('%Y-%m-%dT%H:%M:%S', time.gmtime()), stage))
|
|
196
|
+
progress('Opening project; processor count {0}'.format(Environment.ProcessorCount))
|
|
197
|
+
project = engine.projects.open(request['project'], primary=True)
|
|
198
|
+
# Online changes reuse the downloaded project, which already has this controller.
|
|
199
|
+
if request['operation'] == 'download' and request.get('controller'):
|
|
200
|
+
apply_controller(project, request['controller'], progress)
|
|
201
|
+
application = project.active_application
|
|
202
|
+
online_app = None
|
|
203
|
+
online_device = None
|
|
204
|
+
try:
|
|
205
|
+
progress('Project opened; configuring controller connection')
|
|
206
|
+
if application is None:
|
|
207
|
+
raise RuntimeError('Project has no active Application')
|
|
208
|
+
device, credentials = connect_controller(engine, application, request['online'])
|
|
209
|
+
online_device = engine.online.create_online_device(device)
|
|
210
|
+
if request.get('initializeUser'):
|
|
211
|
+
if request['operation'] != 'download' or not credentials:
|
|
212
|
+
raise RuntimeError('Initial controller user requires an explicit download and credentials')
|
|
213
|
+
online_device.set_credentials_for_initial_user(*credentials)
|
|
214
|
+
progress('Controller connection configured')
|
|
215
|
+
previous = request.get('baseline')
|
|
216
|
+
if request['operation'] != 'download':
|
|
217
|
+
online_app = engine.online.create_online_application(application)
|
|
218
|
+
core = core_application(online_app)
|
|
219
|
+
if not previous or str(application.guid) != previous['applicationGuid']:
|
|
220
|
+
raise RuntimeError('Online change requires the saved downloaded application')
|
|
221
|
+
progress('Checking downloaded controller identity and state')
|
|
222
|
+
session_login(core)
|
|
223
|
+
try:
|
|
224
|
+
observed = controller_ids(core)
|
|
225
|
+
if observed != previous['controllerIds']:
|
|
226
|
+
raise RuntimeError('Controller application differs from the saved baseline; use an explicit download')
|
|
227
|
+
progress('Loading downloaded compiler reference')
|
|
228
|
+
if load_downloaded_reference(application) != observed:
|
|
229
|
+
raise RuntimeError('Downloaded compiler reference differs from the controller; use an explicit download')
|
|
230
|
+
progress('Downloaded compiler reference loaded')
|
|
231
|
+
state = controller_state(core)
|
|
232
|
+
if request['operation'] == 'online-change' and state != 'run':
|
|
233
|
+
raise RuntimeError('Online change requires a running application')
|
|
234
|
+
if request['operation'] == 'inspect':
|
|
235
|
+
return {'applicationGuid': str(application.guid), 'controllerIds': observed,
|
|
236
|
+
'state': state, 'values': {expression: online_app.read_value(expression) for expression in request.get('read', [])}}
|
|
237
|
+
finally:
|
|
238
|
+
core.Logout()
|
|
239
|
+
if request['operation'] == 'online-change':
|
|
240
|
+
reconcile(application, previous['objects'], request['objects'])
|
|
241
|
+
import_objects(engine, application, request, request['operation'] != 'download', progress)
|
|
242
|
+
if request['symbols'] and not any(getattr(child, 'is_symbol_config', False) for child in application.get_children()):
|
|
243
|
+
from System import Guid
|
|
244
|
+
application.create_symbol_config(True, True, Guid('{0141eb75-141b-4ea1-9a8c-75f952b22a6c}'))
|
|
245
|
+
# Match the normal build's source/template lifecycle. Saving the edited
|
|
246
|
+
# tree and clearing editor-load diagnostics happens before compilation.
|
|
247
|
+
project.save()
|
|
248
|
+
for category in engine.system.get_message_categories():
|
|
249
|
+
engine.system.clear_messages(category)
|
|
250
|
+
if request['operation'] == 'download':
|
|
251
|
+
# A full download replaces the previous compiler model. Use the
|
|
252
|
+
# normal build's clean/rebuild path before opening the online session.
|
|
253
|
+
progress('Rebuilding application for full download')
|
|
254
|
+
application.clean()
|
|
255
|
+
application.rebuild()
|
|
256
|
+
fail_on_errors(engine)
|
|
257
|
+
application.generate_code()
|
|
258
|
+
project.save()
|
|
259
|
+
else:
|
|
260
|
+
# Cleaning would discard the downloaded reference needed to decide
|
|
261
|
+
# whether a source edit can be applied as an online change.
|
|
262
|
+
progress('Building application incrementally for online change')
|
|
263
|
+
application.build()
|
|
264
|
+
fail_on_errors(engine)
|
|
265
|
+
if request['operation'] == 'download':
|
|
266
|
+
online_app = engine.online.create_online_application(application)
|
|
267
|
+
core = core_application(online_app)
|
|
268
|
+
progress('Downloading application')
|
|
269
|
+
online_app.login(engine.OnlineChangeOption.Never, False)
|
|
270
|
+
expected = controller_ids(core)
|
|
271
|
+
if controller_state(core) != 'run':
|
|
272
|
+
online_app.start()
|
|
273
|
+
else:
|
|
274
|
+
# Stay on the session-only/core route. High-level scripting Force
|
|
275
|
+
# still auto-creates absent applications; this route never creates
|
|
276
|
+
# an application and passes True explicitly to the download service.
|
|
277
|
+
progress('Rechecking controller identity before online change')
|
|
278
|
+
if not core.Login(True):
|
|
279
|
+
raise RuntimeError('Controller application disappeared before the online change')
|
|
280
|
+
if controller_ids(core) != previous['controllerIds']:
|
|
281
|
+
raise RuntimeError('Controller application changed before the online change')
|
|
282
|
+
expected = controller_ids(core)
|
|
283
|
+
if not application.is_uptodate:
|
|
284
|
+
if not application.is_online_change_possible:
|
|
285
|
+
raise RuntimeError('CODESYS cannot apply this source as an online change; use an explicit download')
|
|
286
|
+
progress('Generating online-change code')
|
|
287
|
+
generate_online_code(application)
|
|
288
|
+
fail_on_errors(engine)
|
|
289
|
+
expected = context_ids(consolidated_manager().GetCompileContext(application.guid))
|
|
290
|
+
mode = loaded_type('_3S.CoDeSys.Core.Online.BootProjectTransferMode')
|
|
291
|
+
progress('Applying online change')
|
|
292
|
+
core.Download(True, Enum.Parse(mode, 'UseProjectSetting'))
|
|
293
|
+
# An online-change transfer does not itself replace the boot project.
|
|
294
|
+
# The original online boot command saves the downloaded application;
|
|
295
|
+
# Boot-only host preparation saves the accepted application separately
|
|
296
|
+
# from the live transfer; it does not issue a controller full download.
|
|
297
|
+
progress('Saving running application as controller boot project')
|
|
298
|
+
create_controller_boot_project(core)
|
|
299
|
+
require_current_boot_project(core)
|
|
300
|
+
progress('Confirming activated code/data identity with a fresh controller login')
|
|
301
|
+
core.Logout()
|
|
302
|
+
session_login(core)
|
|
303
|
+
if controller_ids(core) != expected:
|
|
304
|
+
raise RuntimeError('Controller did not retain the downloaded code/data identity')
|
|
305
|
+
fail_on_errors_after_transfer(engine, progress)
|
|
306
|
+
ids = controller_ids(core)
|
|
307
|
+
state = controller_state(core)
|
|
308
|
+
if state != 'run':
|
|
309
|
+
raise RuntimeError('Controller is not running after the operation')
|
|
310
|
+
progress('Reading controller values')
|
|
311
|
+
result = {'applicationGuid': str(application.guid), 'controllerIds': ids,
|
|
312
|
+
'state': state, 'application': application.get_name(),
|
|
313
|
+
'values': {expression: online_app.read_value(expression) for expression in request.get('read', [])}}
|
|
314
|
+
online_app.logout()
|
|
315
|
+
progress('Saving downloaded project')
|
|
316
|
+
project.save()
|
|
317
|
+
return result
|
|
318
|
+
finally:
|
|
319
|
+
if online_app is not None:
|
|
320
|
+
online_app.Dispose()
|
|
321
|
+
if online_device is not None:
|
|
322
|
+
online_device.Dispose()
|
|
323
|
+
# The original language-model manager joins its asynchronous downloaded
|
|
324
|
+
# reference saver on project close. Publication happens after this returns.
|
|
325
|
+
progress('Closing project and waiting for downloaded reference')
|
|
326
|
+
project.close()
|
|
327
|
+
progress('Project closed')
|