@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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist/cli.js +1696 -0
  4. package/dist/cli.js.map +34 -0
  5. package/dist/index.js +1683 -0
  6. package/dist/index.js.map +31 -0
  7. package/dist/libraries.js +128 -0
  8. package/dist/libraries.js.map +10 -0
  9. package/docker/Dockerfile +173 -0
  10. package/docker/Dockerfile.dockerignore +18 -0
  11. package/docker/Dockerfile.source +15 -0
  12. package/docker/build-image +38 -0
  13. package/docker/codesys-build +5 -0
  14. package/docker/config.ts +8 -0
  15. package/docker/control-linux-sl/Dockerfile +38 -0
  16. package/docker/edge-gateway/Dockerfile +17 -0
  17. package/docker/entrypoint.sh +10 -0
  18. package/docker/install/NOTICE.md +21 -0
  19. package/docker/install/core.sh +36 -0
  20. package/docker/install/decode.c +12 -0
  21. package/docker/install/device/Runtime.ico +0 -0
  22. package/docker/install/device/Runtime.png +0 -0
  23. package/docker/install/device/codesyscontrol.devdesc.xml +1502 -0
  24. package/docker/install/extract.py +43 -0
  25. package/docker/install/helper-launcher.c +46 -0
  26. package/docker/install/install.sh +94 -0
  27. package/docker/install/prerequisites.sh +15 -0
  28. package/docker/install/verify.py +133 -0
  29. package/docker/install/verify.sh +34 -0
  30. package/docker/install/wine-ncrypt/COPYING.LIB +502 -0
  31. package/docker/install/wine-ncrypt/README.md +28 -0
  32. package/docker/install/wine-ncrypt/enable-oaep.py +34 -0
  33. package/docker/install/wine-ncrypt/main.c +619 -0
  34. package/docker/install/wine-ncrypt/oaep.patch +11 -0
  35. package/docker/install/wine-signal/README.md +22 -0
  36. package/docker/install/wine-signal/build.sh +10 -0
  37. package/docker/install/wine-signal/check.sh +34 -0
  38. package/docker/install/wine-signal/noop-i386.c +2 -0
  39. package/docker/install/wine-signal/signal-state.c +105 -0
  40. package/docker/install/wine-signal/tests/avx-registers.c +53 -0
  41. package/docker/install/wine-signal/tests/frame-and-handler.c +192 -0
  42. package/docker/install/wine-signal/tests/loader-i386.c +8 -0
  43. package/docker/install/wine-signal/tests/signal-return.c +47 -0
  44. package/docker/install/wine-signal/wine +4 -0
  45. package/docker/runner.ts +156 -0
  46. package/package.json +53 -4
  47. package/python/build.py +660 -0
  48. package/python/codesys_login.py +100 -0
  49. package/python/codesys_online.py +327 -0
  50. package/python/test_build.py +444 -0
  51. package/python/test_codesys_login.py +120 -0
  52. package/python/test_codesys_online.py +184 -0
  53. package/python/test_installer_packages.py +216 -0
  54. package/python/test_installer_verification.py +307 -0
  55. package/python/test_worker.py +142 -0
  56. package/python/worker.py +102 -0
  57. package/runtime/install.sh +74 -0
  58. package/src/__tests__/fixtures/xml/fb-simple.xml +40 -0
  59. package/src/__tests__/fixtures/xml/fb-state-machine.xml +86 -0
  60. package/src/__tests__/fixtures/xml/prg-cyclic.xml +44 -0
  61. package/src/artifacts.ts +77 -0
  62. package/src/build.ts +87 -0
  63. package/src/cli.ts +116 -0
  64. package/src/config.ts +12 -0
  65. package/src/controller-online.ts +297 -0
  66. package/src/controller-session.ts +298 -0
  67. package/src/diagnostics.ts +68 -0
  68. package/src/docker-build.ts +163 -0
  69. package/src/docker-context.ts +35 -0
  70. package/src/docker-run.ts +103 -0
  71. package/src/host/codesys-command.ts +45 -0
  72. package/src/host/codesys-host.ts +145 -0
  73. package/src/host/codesys-worker.ts +348 -0
  74. package/src/host/windows-path.ts +73 -0
  75. package/src/index.ts +43 -0
  76. package/src/io.ts +127 -0
  77. package/src/libraries.ts +166 -0
  78. package/src/package-path.ts +5 -0
  79. package/src/run.ts +133 -0
  80. package/src/runtime-status.ts +21 -0
  81. package/src/sources.ts +248 -0
  82. package/src/st/declarations.ts +229 -0
  83. package/src/st/lexer.ts +125 -0
  84. package/src/st/parse.ts +202 -0
  85. 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')