openc3 6.4.2 → 6.5.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.
- checksums.yaml +4 -4
- data/bin/openc3cli +172 -97
- data/data/config/_graph_params.yaml +4 -4
- data/data/config/conversions.yaml +2 -1
- data/data/config/item_modifiers.yaml +1 -0
- data/data/config/plugins.yaml +14 -1
- data/data/config/processors.yaml +51 -0
- data/data/config/telemetry_modifiers.yaml +1 -0
- data/lib/openc3/api/tlm_api.rb +10 -5
- data/lib/openc3/microservices/interface_microservice.rb +15 -9
- data/lib/openc3/models/plugin_model.rb +5 -4
- data/lib/openc3/models/scope_model.rb +87 -57
- data/lib/openc3/models/script_engine_model.rb +93 -0
- data/lib/openc3/models/script_status_model.rb +4 -0
- data/lib/openc3/models/target_model.rb +7 -1
- data/lib/openc3/script/script.rb +5 -1
- data/lib/openc3/script_engines/script_engine.rb +118 -0
- data/lib/openc3/topics/interface_topic.rb +23 -3
- data/lib/openc3/utilities/cli_generator.rb +42 -15
- data/lib/openc3/utilities/running_script.rb +1460 -0
- data/lib/openc3/version.rb +6 -6
- data/templates/conversion/conversion.py +1 -1
- data/templates/conversion/conversion.rb +1 -1
- data/templates/processor/processor.py +32 -0
- data/templates/processor/processor.rb +36 -0
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -3
- data/templates/widget/package.json +2 -2
- metadata +7 -1
@@ -0,0 +1,1460 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
3
|
+
# Copyright 2022 Ball Aerospace & Technologies Corp.
|
4
|
+
# All Rights Reserved.
|
5
|
+
#
|
6
|
+
# This program is free software; you can modify and/or redistribute it
|
7
|
+
# under the terms of the GNU Affero General Public License
|
8
|
+
# as published by the Free Software Foundation; version 3 with
|
9
|
+
# attribution addendums as found in the LICENSE.txt
|
10
|
+
#
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Affero General Public License for more details.
|
15
|
+
|
16
|
+
# Modified by OpenC3, Inc.
|
17
|
+
# All changes Copyright 2025, OpenC3, Inc.
|
18
|
+
# All Rights Reserved
|
19
|
+
#
|
20
|
+
# This file may also be used under the terms of a commercial license
|
21
|
+
# if purchased from OpenC3, Inc.
|
22
|
+
|
23
|
+
require 'json'
|
24
|
+
require 'securerandom'
|
25
|
+
require 'openc3'
|
26
|
+
require 'openc3/utilities/bucket_utilities'
|
27
|
+
require 'openc3/script'
|
28
|
+
require 'openc3/io/stdout'
|
29
|
+
require 'openc3/io/stderr'
|
30
|
+
require 'childprocess'
|
31
|
+
require 'openc3/script/suite_runner'
|
32
|
+
require 'openc3/utilities/store'
|
33
|
+
require 'openc3/utilities/store_queued'
|
34
|
+
require 'openc3/utilities/bucket_require'
|
35
|
+
require 'openc3/models/offline_access_model'
|
36
|
+
require 'openc3/models/environment_model'
|
37
|
+
require 'openc3/models/script_engine_model'
|
38
|
+
require 'openc3/models/script_status_model'
|
39
|
+
|
40
|
+
if not defined? RAILS_ROOT
|
41
|
+
if ENV['RAILS_ROOT']
|
42
|
+
RAILS_ROOT = ENV['RAILS_ROOT']
|
43
|
+
elsif defined? Rails
|
44
|
+
RAILS_ROOT = Rails.root
|
45
|
+
else
|
46
|
+
RAILS_ROOT = File.expand_path(File.join(__dir__, '..', '..'))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
SCRIPT_API = 'script-api'
|
51
|
+
|
52
|
+
def running_script_publish(channel_name, data)
|
53
|
+
stream_name = [SCRIPT_API, channel_name].compact.join(":")
|
54
|
+
OpenC3::Store.publish(stream_name, JSON.generate(data))
|
55
|
+
end
|
56
|
+
|
57
|
+
def running_script_anycable_publish(channel_name, data)
|
58
|
+
stream_name = [SCRIPT_API, channel_name].compact.join(":")
|
59
|
+
stream_data = {"stream" => stream_name, "data" => JSON.generate(data)}
|
60
|
+
OpenC3::Store.publish("__anycable__", JSON.generate(stream_data))
|
61
|
+
end
|
62
|
+
|
63
|
+
module OpenC3
|
64
|
+
module Script
|
65
|
+
private
|
66
|
+
# Define all the user input methods used in scripting which we need to broadcast to the frontend
|
67
|
+
# Note: This list matches the list in run_script.rb:116
|
68
|
+
SCRIPT_METHODS = %i[ask ask_string message_box vertical_message_box combo_box prompt prompt_for_hazardous
|
69
|
+
prompt_for_critical_cmd metadata_input open_file_dialog open_files_dialog]
|
70
|
+
SCRIPT_METHODS.each do |method|
|
71
|
+
define_method(method) do |*args, **kwargs|
|
72
|
+
while true
|
73
|
+
if RunningScript.instance
|
74
|
+
RunningScript.instance.scriptrunner_puts("#{method}(#{args.join(', ')})")
|
75
|
+
prompt_id = SecureRandom.uuid
|
76
|
+
RunningScript.instance.perform_wait({ 'method' => method, 'id' => prompt_id, 'args' => args, 'kwargs' => kwargs })
|
77
|
+
input = RunningScript.instance.user_input
|
78
|
+
# All ask and prompt dialogs should include a 'Cancel' button
|
79
|
+
# If they cancel we wait so they can potentially stop
|
80
|
+
if input == 'Cancel'
|
81
|
+
RunningScript.instance.perform_pause
|
82
|
+
else
|
83
|
+
if (method.to_s.include?('open_file'))
|
84
|
+
files = input.map do |filename|
|
85
|
+
file = _get_storage_file("tmp/#{filename}", scope: RunningScript.instance.scope)
|
86
|
+
# Set filename method we added to Tempfile in the core_ext
|
87
|
+
file.filename = filename
|
88
|
+
file
|
89
|
+
end
|
90
|
+
files = files[0] if method.to_s == 'open_file_dialog' # Simply return the only file
|
91
|
+
return files
|
92
|
+
elsif method.to_s == 'prompt_for_critical_cmd'
|
93
|
+
if input == 'REJECTED'
|
94
|
+
raise "Critical Cmd Rejected"
|
95
|
+
end
|
96
|
+
return input
|
97
|
+
else
|
98
|
+
return input
|
99
|
+
end
|
100
|
+
end
|
101
|
+
else
|
102
|
+
raise "Script input method called outside of running script"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def step_mode
|
109
|
+
RunningScript.instance.step
|
110
|
+
end
|
111
|
+
|
112
|
+
def run_mode
|
113
|
+
RunningScript.instance.go
|
114
|
+
end
|
115
|
+
|
116
|
+
OpenC3.disable_warnings do
|
117
|
+
def start(procedure_name, line_no: 1, end_line_no: nil, bind_variables: false, complete: false)
|
118
|
+
RunningScript.instance.execute_while_paused_info = nil
|
119
|
+
path = procedure_name
|
120
|
+
|
121
|
+
# Decide if using script engine
|
122
|
+
use_script_engine = false
|
123
|
+
if RunningScript.instance.script_engine and procedure_name
|
124
|
+
extension = File.extname(procedure_name).to_s.downcase
|
125
|
+
if extension != ".rb"
|
126
|
+
use_script_engine = true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check RAM based instrumented cache
|
131
|
+
breakpoints = RunningScript.breakpoints[path]&.filter { |_, present| present }&.map { |line_number, _| line_number - 1 } # -1 because frontend lines are 0-indexed
|
132
|
+
breakpoints ||= []
|
133
|
+
|
134
|
+
instrumented_script = nil
|
135
|
+
instrumented_cache = nil
|
136
|
+
text = nil
|
137
|
+
if line_no == 1 and end_line_no.nil?
|
138
|
+
instrumented_cache, text = RunningScript.instrumented_cache[path]
|
139
|
+
end
|
140
|
+
|
141
|
+
if instrumented_cache
|
142
|
+
# Use cached instrumentation
|
143
|
+
instrumented_script = instrumented_cache
|
144
|
+
cached = true
|
145
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :file, filename: procedure_name, text: text.to_utf8, breakpoints: breakpoints })
|
146
|
+
else
|
147
|
+
# Retrieve file
|
148
|
+
text = ::Script.body(RunningScript.instance.scope, procedure_name)
|
149
|
+
raise "Unable to retrieve: #{procedure_name}" unless text
|
150
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :file, filename: procedure_name, text: text.to_utf8, breakpoints: breakpoints })
|
151
|
+
|
152
|
+
# Cache instrumentation into RAM
|
153
|
+
if line_no == 1 and end_line_no.nil?
|
154
|
+
if use_script_engine
|
155
|
+
# Don't instrument if using a script engine
|
156
|
+
instrumented_script = text
|
157
|
+
else
|
158
|
+
instrumented_script = RunningScript.instrument_script(text, path, true)
|
159
|
+
end
|
160
|
+
RunningScript.instrumented_cache[path] = [instrumented_script, text]
|
161
|
+
else
|
162
|
+
if line_no > 1 or not end_line_no.nil?
|
163
|
+
text_lines = text.lines
|
164
|
+
|
165
|
+
# Instrument only the specified lines
|
166
|
+
if end_line_no.nil?
|
167
|
+
end_line_no = text_lines.length
|
168
|
+
end
|
169
|
+
|
170
|
+
if line_no < 1 or line_no > text_lines.length
|
171
|
+
raise "Invalid start line number: #{line_no} for #{procedure_name}"
|
172
|
+
end
|
173
|
+
|
174
|
+
if end_line_no < 1 or end_line_no > text_lines.length
|
175
|
+
raise "Invalid end line number: #{end_line_no} for #{procedure_name}"
|
176
|
+
end
|
177
|
+
|
178
|
+
if line_no > end_line_no
|
179
|
+
raise "Start line number #{line_no} is greater than end line number #{end_line_no} for #{procedure_name}"
|
180
|
+
end
|
181
|
+
|
182
|
+
if not use_script_engine
|
183
|
+
text = text_lines[(line_no - 1)...end_line_no].join
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
if use_script_engine
|
188
|
+
instrumented_script = text
|
189
|
+
else
|
190
|
+
if bind_variables
|
191
|
+
instrumented_script = RunningScript.instrument_script(text, path, false, line_offset: line_no - 1, cache: false)
|
192
|
+
else
|
193
|
+
instrumented_script = RunningScript.instrument_script(text, path, true, line_offset: line_no - 1, cache: false)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
cached = false
|
199
|
+
end
|
200
|
+
running = ScriptStatusModel.all(scope: RunningScript.instance.scope, type: 'running')
|
201
|
+
running_script_anycable_publish("all-scripts-channel", { type: :start, filename: procedure_name, active_scripts: running.length, scope: RunningScript.instance.scope })
|
202
|
+
|
203
|
+
if use_script_engine
|
204
|
+
if line_no != 1 or !end_line_no.nil?
|
205
|
+
if end_line_no.nil?
|
206
|
+
# Goto line
|
207
|
+
RunningScript.instance.script_engine.run_text(instrumented_script, filename: procedure_name, line_no: line_no)
|
208
|
+
else
|
209
|
+
# Execute selection
|
210
|
+
RunningScript.instance.script_engine.run_text(instrumented_script, filename: procedure_name, line_no: line_no, end_line_no: end_line_no)
|
211
|
+
end
|
212
|
+
else
|
213
|
+
RunningScript.instance.script_engine.run_text(instrumented_script, filename: procedure_name)
|
214
|
+
end
|
215
|
+
else
|
216
|
+
if bind_variables
|
217
|
+
eval(instrumented_script, RunningScript.instance.script_binding, path, line_no)
|
218
|
+
else
|
219
|
+
Object.class_eval(instrumented_script, path, line_no)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
if complete
|
224
|
+
RunningScript.instance.script_status.state = 'completed'
|
225
|
+
RunningScript.instance.script_status.end_time = Time.now.utc.iso8601
|
226
|
+
RunningScript.instance.script_status.update(queued: true)
|
227
|
+
raise OpenC3::StopScript
|
228
|
+
end
|
229
|
+
|
230
|
+
# Return whether we had to load and instrument this file, i.e. it was not cached
|
231
|
+
!cached
|
232
|
+
end
|
233
|
+
|
234
|
+
def goto(line_no_or_procedure_name, line_no = nil)
|
235
|
+
if line_no.nil?
|
236
|
+
start(RunningScript.instance.current_filename, line_no: line_no_or_procedure_name, bind_variables: true, complete: true)
|
237
|
+
else
|
238
|
+
start(line_no_or_procedure_name, line_no: line_no, bind_variables: true, complete: true)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Require an additional ruby file
|
243
|
+
def load_utility(procedure_name)
|
244
|
+
# Ensure load_utility works like require where you don't need the .rb extension
|
245
|
+
if File.extname(procedure_name) != '.rb'
|
246
|
+
procedure_name += '.rb'
|
247
|
+
end
|
248
|
+
not_cached = false
|
249
|
+
if defined? RunningScript and RunningScript.instance
|
250
|
+
saved = RunningScript.instance.use_instrumentation
|
251
|
+
begin
|
252
|
+
RunningScript.instance.use_instrumentation = false
|
253
|
+
not_cached = start(procedure_name)
|
254
|
+
ensure
|
255
|
+
RunningScript.instance.use_instrumentation = saved
|
256
|
+
end
|
257
|
+
else # Just call require
|
258
|
+
not_cached = require(procedure_name)
|
259
|
+
end
|
260
|
+
# Return whether we had to load and instrument this file, i.e. it was not cached
|
261
|
+
# This is designed to match the behavior of Ruby's require and load keywords
|
262
|
+
not_cached
|
263
|
+
end
|
264
|
+
alias require_utility load_utility
|
265
|
+
|
266
|
+
# sleep in a script - returns true if canceled mid sleep
|
267
|
+
def openc3_script_sleep(sleep_time = nil)
|
268
|
+
return true if $disconnect
|
269
|
+
RunningScript.instance.update_running_script_store("waiting")
|
270
|
+
if RunningScript.instance.use_instrumentation
|
271
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :line, filename: RunningScript.instance.current_filename, line_no: RunningScript.instance.current_line_number, state: :waiting })
|
272
|
+
end
|
273
|
+
|
274
|
+
sleep_time = 30000000 unless sleep_time # Handle infinite wait
|
275
|
+
if sleep_time > 0.0
|
276
|
+
end_time = Time.now.sys + sleep_time
|
277
|
+
count = 0
|
278
|
+
until Time.now.sys >= end_time
|
279
|
+
sleep(0.01)
|
280
|
+
count += 1
|
281
|
+
if RunningScript.instance.use_instrumentation and (count % 100) == 0 # Approximately Every Second
|
282
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :line, filename: RunningScript.instance.current_filename, line_no: RunningScript.instance.current_line_number, state: :waiting })
|
283
|
+
end
|
284
|
+
if RunningScript.instance.pause?
|
285
|
+
RunningScript.instance.perform_pause
|
286
|
+
return true
|
287
|
+
end
|
288
|
+
return true if RunningScript.instance.go?
|
289
|
+
raise StopScript if RunningScript.instance.stop?
|
290
|
+
end
|
291
|
+
end
|
292
|
+
return false
|
293
|
+
end
|
294
|
+
|
295
|
+
def display_screen(target_name, screen_name, x = nil, y = nil, scope: RunningScript.instance.scope)
|
296
|
+
definition = get_screen_definition(target_name, screen_name, scope: scope)
|
297
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :screen, target_name: target_name, screen_name: screen_name, definition: definition, x: x, y: y })
|
298
|
+
end
|
299
|
+
|
300
|
+
def clear_screen(target_name, screen_name)
|
301
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :clearscreen, target_name: target_name, screen_name: screen_name })
|
302
|
+
end
|
303
|
+
|
304
|
+
def clear_all_screens
|
305
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :clearallscreens })
|
306
|
+
end
|
307
|
+
|
308
|
+
def local_screen(screen_name, definition, x = nil, y = nil)
|
309
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :screen, target_name: "LOCAL", screen_name: screen_name, definition: definition, x: x, y: y })
|
310
|
+
end
|
311
|
+
|
312
|
+
def download_file(path, scope: RunningScript.instance.scope)
|
313
|
+
url = _get_download_url(path, scope: scope)
|
314
|
+
running_script_anycable_publish("running-script-channel:#{RunningScript.instance.id}", { type: :downloadfile, filename: File.basename(path), url: url })
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
class RunningScript
|
321
|
+
def id
|
322
|
+
return @script_status.id
|
323
|
+
end
|
324
|
+
def scope
|
325
|
+
return @script_status.scope
|
326
|
+
end
|
327
|
+
def filename
|
328
|
+
return @script_status.filename
|
329
|
+
end
|
330
|
+
def current_filename
|
331
|
+
return @script_status.current_filename
|
332
|
+
end
|
333
|
+
def current_line_number
|
334
|
+
return @script_status.line_no
|
335
|
+
end
|
336
|
+
|
337
|
+
attr_accessor :use_instrumentation
|
338
|
+
attr_accessor :continue_after_error
|
339
|
+
attr_accessor :exceptions
|
340
|
+
attr_accessor :script_binding
|
341
|
+
attr_accessor :script_engine
|
342
|
+
attr_accessor :user_input
|
343
|
+
attr_accessor :prompt_id
|
344
|
+
attr_reader :script_status
|
345
|
+
attr_accessor :execute_while_paused_info
|
346
|
+
|
347
|
+
# This REGEX is also found in scripts_controller.rb
|
348
|
+
# Matches the following test cases:
|
349
|
+
# class MySuite < TestSuite
|
350
|
+
# class MySuite < OpenC3::Suite
|
351
|
+
# class MySuite < Cosmos::TestSuite
|
352
|
+
# class MySuite < Suite # comment
|
353
|
+
# # class MySuite < Suite # <-- doesn't match commented out
|
354
|
+
SUITE_REGEX = /^(\s*)?class\s+\w+\s+<\s+(Cosmos::|OpenC3::)?(Suite|TestSuite)/
|
355
|
+
|
356
|
+
@@instance = nil
|
357
|
+
@@message_log = nil
|
358
|
+
@@run_thread = nil
|
359
|
+
@@breakpoints = {}
|
360
|
+
@@line_delay = 0.1
|
361
|
+
@@max_output_characters = 50000
|
362
|
+
@@instrumented_cache = {}
|
363
|
+
@@file_cache = {}
|
364
|
+
@@output_thread = nil
|
365
|
+
@@pause_on_error = true
|
366
|
+
@@error = nil
|
367
|
+
@@output_sleeper = OpenC3::Sleeper.new
|
368
|
+
@@cancel_output = false
|
369
|
+
|
370
|
+
def self.message_log
|
371
|
+
return @@message_log if @@message_log
|
372
|
+
|
373
|
+
if @@instance
|
374
|
+
scope = @@instance.scope
|
375
|
+
tags = [File.basename(@@instance.filename, '.rb').gsub(/(\s|\W)/, '_')]
|
376
|
+
else
|
377
|
+
scope = $openc3_scope
|
378
|
+
tags = []
|
379
|
+
end
|
380
|
+
@@message_log = OpenC3::MessageLog.new("sr", File.join(RAILS_ROOT, 'log'), tags: tags, scope: scope)
|
381
|
+
end
|
382
|
+
|
383
|
+
def message_log
|
384
|
+
self.class.message_log
|
385
|
+
end
|
386
|
+
|
387
|
+
def self.spawn(scope, name, suite_runner = nil, disconnect = false, environment = nil, user_full_name = nil, username = nil, line_no = nil, end_line_no = nil)
|
388
|
+
extension = File.extname(name).to_s.downcase
|
389
|
+
script_engine = nil
|
390
|
+
if extension == '.py'
|
391
|
+
process_name = 'python'
|
392
|
+
runner_path = File.join(RAILS_ROOT, 'scripts', 'run_script.py')
|
393
|
+
elsif extension == '.rb'
|
394
|
+
process_name = 'ruby'
|
395
|
+
runner_path = File.join(RAILS_ROOT, 'scripts', 'run_script.rb')
|
396
|
+
else
|
397
|
+
raise "Suite Runner is not supported for this file type: #{extension}" if suite_runner
|
398
|
+
|
399
|
+
# Extension possibly supported by a script engine
|
400
|
+
script_engine_model = OpenC3::ScriptEngineModel.get_model(name: extension, scope: scope)
|
401
|
+
if script_engine_model
|
402
|
+
script_engine = script_engine_model.filename
|
403
|
+
if File.extname(script_engine).to_s.downcase == '.py'
|
404
|
+
process_name = 'python'
|
405
|
+
runner_path = File.join(RAILS_ROOT, 'scripts', 'run_script.py')
|
406
|
+
else
|
407
|
+
process_name = 'ruby'
|
408
|
+
runner_path = File.join(RAILS_ROOT, 'scripts', 'run_script.rb')
|
409
|
+
end
|
410
|
+
else
|
411
|
+
raise "Unsupported script file type: #{extension}"
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
running_script_id = OpenC3::Store.incr('running-script-id')
|
416
|
+
|
417
|
+
# COSMOS Core username (Enterprise has the actual name)
|
418
|
+
username ||= 'Anonymous'
|
419
|
+
# COSMOS Core full name (Enterprise has the actual name)
|
420
|
+
user_full_name ||= 'Anonymous'
|
421
|
+
start_time = Time.now.utc.iso8601
|
422
|
+
|
423
|
+
process = ChildProcess.build(process_name, runner_path.to_s, running_script_id.to_s, scope)
|
424
|
+
process.io.inherit! # Helps with debugging
|
425
|
+
process.cwd = File.join(RAILS_ROOT, 'scripts')
|
426
|
+
|
427
|
+
# Check for offline access token
|
428
|
+
model = nil
|
429
|
+
model = OpenC3::OfflineAccessModel.get_model(name: username, scope: scope) if username != 'Anonymous'
|
430
|
+
|
431
|
+
# Load the global environment variables
|
432
|
+
status_environment = {}
|
433
|
+
values = OpenC3::EnvironmentModel.all(scope: scope).values
|
434
|
+
values.each do |env|
|
435
|
+
process.environment[env['key']] = env['value']
|
436
|
+
status_environment[env['key']] = env['value']
|
437
|
+
end
|
438
|
+
# Load the script specific ENV vars set by the GUI
|
439
|
+
# These can override the previously defined global env vars
|
440
|
+
if environment
|
441
|
+
environment.each do |env|
|
442
|
+
process.environment[env['key']] = env['value']
|
443
|
+
status_environment[env['key']] = env['value']
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
script_status = OpenC3::ScriptStatusModel.new(
|
448
|
+
name: running_script_id.to_s, # Unique id for this script
|
449
|
+
state: 'spawning', # State will be spawning until the script is running
|
450
|
+
shard: 0, # Future enhancement of script runner shards
|
451
|
+
filename: name, # Initial filename never changes
|
452
|
+
current_filename: name, # Current filename updates while we are running
|
453
|
+
line_no: 0, # 0 means not running yet
|
454
|
+
start_line_no: line_no || 1, # Line number to start running the script
|
455
|
+
end_line_no: end_line_no || nil, # Line number to stop running the script
|
456
|
+
username: username, # username of the person who started the script
|
457
|
+
user_full_name: user_full_name, # full name of the person who started the script
|
458
|
+
start_time: start_time, # Time the script started ISO format
|
459
|
+
end_time: nil, # Time the script ended ISO format
|
460
|
+
disconnect: disconnect, # Disconnect is set to true if the script is running in a disconnected mode
|
461
|
+
environment: status_environment.as_json(:allow_nan => true).to_json(:allow_nan => true), # nil or Hash of key/value pairs for environment variables
|
462
|
+
suite_runner: suite_runner ? suite_runner.as_json(:allow_nan => true).to_json(:allow_nan => true) : nil,
|
463
|
+
errors: nil, # array of errors that occurred during the script run
|
464
|
+
pid: nil, # pid of the script process - set by the script itself when it starts
|
465
|
+
script_engine: script_engine, # script engine filename
|
466
|
+
updated_at: nil, # Set by create/update - ISO format
|
467
|
+
scope: scope # Scope of the script
|
468
|
+
)
|
469
|
+
script_status.create(isoformat: true)
|
470
|
+
|
471
|
+
# Set proper secrets for running script
|
472
|
+
process.environment['SECRET_KEY_BASE'] = nil
|
473
|
+
process.environment['OPENC3_REDIS_USERNAME'] = ENV['OPENC3_SR_REDIS_USERNAME']
|
474
|
+
process.environment['OPENC3_REDIS_PASSWORD'] = ENV['OPENC3_SR_REDIS_PASSWORD']
|
475
|
+
process.environment['OPENC3_BUCKET_USERNAME'] = ENV['OPENC3_SR_BUCKET_USERNAME']
|
476
|
+
process.environment['OPENC3_BUCKET_PASSWORD'] = ENV['OPENC3_SR_BUCKET_PASSWORD']
|
477
|
+
process.environment['OPENC3_SR_REDIS_USERNAME'] = nil
|
478
|
+
process.environment['OPENC3_SR_REDIS_PASSWORD'] = nil
|
479
|
+
process.environment['OPENC3_SR_BUCKET_USERNAME'] = nil
|
480
|
+
process.environment['OPENC3_SR_BUCKET_PASSWORD'] = nil
|
481
|
+
process.environment['OPENC3_API_CLIENT'] = ENV['OPENC3_API_CLIENT']
|
482
|
+
if model and model.offline_access_token
|
483
|
+
auth = OpenC3::OpenC3KeycloakAuthentication.new(ENV['OPENC3_KEYCLOAK_URL'])
|
484
|
+
valid_token = auth.get_token_from_refresh_token(model.offline_access_token)
|
485
|
+
if valid_token
|
486
|
+
process.environment['OPENC3_API_TOKEN'] = model.offline_access_token
|
487
|
+
else
|
488
|
+
model.offline_access_token = nil
|
489
|
+
model.update
|
490
|
+
raise "offline_access token invalid for script"
|
491
|
+
end
|
492
|
+
else
|
493
|
+
process.environment['OPENC3_API_USER'] = ENV['OPENC3_API_USER']
|
494
|
+
if ENV['OPENC3_SERVICE_PASSWORD']
|
495
|
+
process.environment['OPENC3_API_PASSWORD'] = ENV['OPENC3_SERVICE_PASSWORD']
|
496
|
+
else
|
497
|
+
raise "No authentication available for script"
|
498
|
+
end
|
499
|
+
end
|
500
|
+
process.environment['GEM_HOME'] = ENV['GEM_HOME']
|
501
|
+
process.environment['PYTHONUSERBASE'] = ENV['PYTHONUSERBASE']
|
502
|
+
|
503
|
+
# Spawned process should not be controlled by same Bundler constraints as spawning process
|
504
|
+
ENV.each do |key, _value|
|
505
|
+
if key =~ /^BUNDLE/
|
506
|
+
process.environment[key] = nil
|
507
|
+
end
|
508
|
+
end
|
509
|
+
process.environment['RUBYOPT'] = nil # Removes loading bundler setup
|
510
|
+
process.environment['OPENC3_SCOPE'] = scope
|
511
|
+
process.environment['RAILS_ROOT'] = RAILS_ROOT
|
512
|
+
|
513
|
+
process.detach = true
|
514
|
+
process.start
|
515
|
+
running_script_id
|
516
|
+
end
|
517
|
+
|
518
|
+
def initialize(script_status)
|
519
|
+
@@instance = self
|
520
|
+
@script_status = script_status
|
521
|
+
@script_status.pid = Process.pid
|
522
|
+
@user_input = ''
|
523
|
+
@prompt_id = nil
|
524
|
+
@line_offset = 0
|
525
|
+
@output_io = StringIO.new('', 'r+')
|
526
|
+
@output_io_mutex = Mutex.new
|
527
|
+
@continue_after_error = true
|
528
|
+
@debug_text = nil
|
529
|
+
@debug_history = []
|
530
|
+
@debug_code_completion = nil
|
531
|
+
@output_time = Time.now.sys
|
532
|
+
|
533
|
+
initialize_variables()
|
534
|
+
update_running_script_store("init")
|
535
|
+
redirect_io() # Redirect $stdout and $stderr
|
536
|
+
mark_breakpoints(@script_status.filename)
|
537
|
+
disconnect_script() if @script_status.disconnect
|
538
|
+
|
539
|
+
@script_engine = nil
|
540
|
+
if @script_status.script_engine
|
541
|
+
klass = OpenC3.require_class(@script_status.script_engine)
|
542
|
+
@script_engine = klass.new(self)
|
543
|
+
end
|
544
|
+
|
545
|
+
# Retrieve file
|
546
|
+
@body = ::Script.body(@script_status.scope, @script_status.filename)
|
547
|
+
raise "Script not found: #{@script_status.filename}" if @body.nil?
|
548
|
+
breakpoints = @@breakpoints[@script_status.filename]&.filter { |_, present| present }&.map { |line_number, _| line_number - 1 } # -1 because frontend lines are 0-indexed
|
549
|
+
breakpoints ||= []
|
550
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :file, filename: @script_status.filename, scope: @script_status.scope, text: @body.to_utf8, breakpoints: breakpoints })
|
551
|
+
if not @script_status.script_engine and @body =~ SUITE_REGEX
|
552
|
+
# Process the suite file in this context so we can load it
|
553
|
+
# TODO: Do we need to worry about success or failure of the suite processing?
|
554
|
+
::Script.process_suite(@script_status.filename, @body, new_process: false, scope: @script_status.scope)
|
555
|
+
# Call load_utility to parse the suite and allow for individual methods to be executed
|
556
|
+
load_utility(@script_status.filename)
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
# Called to update the running script state every time the state or line_no changes
|
561
|
+
def update_running_script_store(state = nil)
|
562
|
+
@script_status.state = state if state
|
563
|
+
@script_status.update(queued: true)
|
564
|
+
end
|
565
|
+
|
566
|
+
def parse_options(options)
|
567
|
+
settings = {}
|
568
|
+
if options.include?('manual')
|
569
|
+
settings['Manual'] = true
|
570
|
+
$manual = true
|
571
|
+
else
|
572
|
+
settings['Manual'] = false
|
573
|
+
$manual = false
|
574
|
+
end
|
575
|
+
if options.include?('pauseOnError')
|
576
|
+
settings['Pause on Error'] = true
|
577
|
+
@@pause_on_error = true
|
578
|
+
else
|
579
|
+
settings['Pause on Error'] = false
|
580
|
+
@@pause_on_error = false
|
581
|
+
end
|
582
|
+
if options.include?('continueAfterError')
|
583
|
+
settings['Continue After Error'] = true
|
584
|
+
@continue_after_error = true
|
585
|
+
else
|
586
|
+
settings['Continue After Error'] = false
|
587
|
+
@continue_after_error = false
|
588
|
+
end
|
589
|
+
if options.include?('abortAfterError')
|
590
|
+
settings['Abort After Error'] = true
|
591
|
+
OpenC3::Test.abort_on_exception = true
|
592
|
+
else
|
593
|
+
settings['Abort After Error'] = false
|
594
|
+
OpenC3::Test.abort_on_exception = false
|
595
|
+
end
|
596
|
+
if options.include?('loop')
|
597
|
+
settings['Loop'] = true
|
598
|
+
else
|
599
|
+
settings['Loop'] = false
|
600
|
+
end
|
601
|
+
if options.include?('breakLoopOnError')
|
602
|
+
settings['Break Loop On Error'] = true
|
603
|
+
else
|
604
|
+
settings['Break Loop On Error'] = false
|
605
|
+
end
|
606
|
+
OpenC3::SuiteRunner.settings = settings
|
607
|
+
end
|
608
|
+
|
609
|
+
# Let the script continue pausing if in step mode
|
610
|
+
def continue
|
611
|
+
@go = true
|
612
|
+
@pause = true if @step
|
613
|
+
end
|
614
|
+
|
615
|
+
# Sets step mode and lets the script continue but with pause set
|
616
|
+
def step
|
617
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :step, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
618
|
+
@step = true
|
619
|
+
@go = true
|
620
|
+
@pause = true
|
621
|
+
end
|
622
|
+
|
623
|
+
# Clears step mode and lets the script continue
|
624
|
+
def go
|
625
|
+
@step = false
|
626
|
+
@go = true
|
627
|
+
@pause = false
|
628
|
+
end
|
629
|
+
|
630
|
+
def go?
|
631
|
+
temp = @go
|
632
|
+
@go = false
|
633
|
+
temp
|
634
|
+
end
|
635
|
+
|
636
|
+
def pause
|
637
|
+
@pause = true
|
638
|
+
@go = false
|
639
|
+
end
|
640
|
+
|
641
|
+
def pause?
|
642
|
+
@pause
|
643
|
+
end
|
644
|
+
|
645
|
+
def stop
|
646
|
+
if @@run_thread
|
647
|
+
@stop = true
|
648
|
+
@script_status.end_time = Time.now.utc.iso8601
|
649
|
+
update_running_script_store("stopped")
|
650
|
+
OpenC3.kill_thread(self, @@run_thread)
|
651
|
+
@@run_thread = nil
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
def stop?
|
656
|
+
@stop
|
657
|
+
end
|
658
|
+
|
659
|
+
def clear_prompt
|
660
|
+
# Allow things to continue once the prompt is cleared
|
661
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :script, prompt_complete: @prompt_id })
|
662
|
+
@prompt_id = nil
|
663
|
+
end
|
664
|
+
|
665
|
+
# Private methods
|
666
|
+
|
667
|
+
def graceful_kill
|
668
|
+
@stop = true
|
669
|
+
end
|
670
|
+
|
671
|
+
def initialize_variables
|
672
|
+
@@error = nil
|
673
|
+
@go = false
|
674
|
+
@pause = false
|
675
|
+
@step = false
|
676
|
+
@stop = false
|
677
|
+
@retry_needed = false
|
678
|
+
@use_instrumentation = true
|
679
|
+
@call_stack = []
|
680
|
+
@pre_line_time = Time.now.sys
|
681
|
+
@exceptions = nil
|
682
|
+
@script_binding = nil
|
683
|
+
@inline_eval = nil
|
684
|
+
@script_status.current_filename = @script_status.filename
|
685
|
+
@script_status.line_no = 0
|
686
|
+
@current_file = nil
|
687
|
+
@execute_while_paused_info = nil
|
688
|
+
end
|
689
|
+
|
690
|
+
def unique_filename
|
691
|
+
if @script_status.filename and !@script_status.filename.empty?
|
692
|
+
return @script_status.filename
|
693
|
+
else
|
694
|
+
return "Untitled" + @script_status.id.to_s
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
def stop_message_log
|
699
|
+
metadata = {
|
700
|
+
"id" => @script_status.id,
|
701
|
+
"user" => @script_status.username,
|
702
|
+
"scriptname" => unique_filename()
|
703
|
+
}
|
704
|
+
if @@message_log
|
705
|
+
@script_status.log = @@message_log.stop(true, metadata: metadata)
|
706
|
+
@script_status.update()
|
707
|
+
end
|
708
|
+
@@message_log = nil
|
709
|
+
end
|
710
|
+
|
711
|
+
def self.instance
|
712
|
+
@@instance
|
713
|
+
end
|
714
|
+
|
715
|
+
def self.instance=(value)
|
716
|
+
@@instance = value
|
717
|
+
end
|
718
|
+
|
719
|
+
def self.line_delay
|
720
|
+
@@line_delay
|
721
|
+
end
|
722
|
+
|
723
|
+
def self.line_delay=(value)
|
724
|
+
@@line_delay = value
|
725
|
+
end
|
726
|
+
|
727
|
+
def self.max_output_characters
|
728
|
+
@@max_output_characters
|
729
|
+
end
|
730
|
+
|
731
|
+
def self.max_output_characters=(value)
|
732
|
+
@@max_output_characters = value
|
733
|
+
end
|
734
|
+
|
735
|
+
def self.breakpoints
|
736
|
+
@@breakpoints
|
737
|
+
end
|
738
|
+
|
739
|
+
def self.instrumented_cache
|
740
|
+
@@instrumented_cache
|
741
|
+
end
|
742
|
+
|
743
|
+
def self.instrumented_cache=(value)
|
744
|
+
@@instrumented_cache = value
|
745
|
+
end
|
746
|
+
|
747
|
+
def self.file_cache
|
748
|
+
@@file_cache
|
749
|
+
end
|
750
|
+
|
751
|
+
def self.file_cache=(value)
|
752
|
+
@@file_cache = value
|
753
|
+
end
|
754
|
+
|
755
|
+
def self.pause_on_error
|
756
|
+
@@pause_on_error
|
757
|
+
end
|
758
|
+
|
759
|
+
def self.pause_on_error=(value)
|
760
|
+
@@pause_on_error = value
|
761
|
+
end
|
762
|
+
|
763
|
+
def text
|
764
|
+
@body
|
765
|
+
end
|
766
|
+
|
767
|
+
def retry_needed
|
768
|
+
@retry_needed = true
|
769
|
+
end
|
770
|
+
|
771
|
+
def run
|
772
|
+
if @script_status.suite_runner
|
773
|
+
@script_status.suite_runner = JSON.parse(@script_status.suite_runner, :allow_nan => true, :create_additions => true) # Convert to hash
|
774
|
+
parse_options(@script_status.suite_runner['options'])
|
775
|
+
if @script_status.suite_runner['script']
|
776
|
+
run_text("OpenC3::SuiteRunner.start(#{@script_status.suite_runner['suite']}, #{@script_status.suite_runner['group']}, '#{@script_status.suite_runner['script']}')", initial_filename: "SCRIPTRUNNER")
|
777
|
+
elsif script_status.suite_runner['group']
|
778
|
+
run_text("OpenC3::SuiteRunner.#{@script_status.suite_runner['method']}(#{@script_status.suite_runner['suite']}, #{@script_status.suite_runner['group']})", initial_filename: "SCRIPTRUNNER")
|
779
|
+
else
|
780
|
+
run_text("OpenC3::SuiteRunner.#{@script_status.suite_runner['method']}(#{@script_status.suite_runner['suite']})", initial_filename: "SCRIPTRUNNER")
|
781
|
+
end
|
782
|
+
else
|
783
|
+
if not @script_engine and (@script_status.start_line_no != 1 or !@script_status.end_line_no.nil?)
|
784
|
+
run_text("", initial_filename: "SCRIPTRUNNER")
|
785
|
+
else
|
786
|
+
run_text(@body)
|
787
|
+
end
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
def self.instrument_script(text, filename, mark_private = false, line_offset: 0, cache: true)
|
792
|
+
if cache and filename and !filename.empty?
|
793
|
+
@@file_cache[filename] = text.clone
|
794
|
+
end
|
795
|
+
|
796
|
+
ruby_lex_utils = RubyLexUtils.new
|
797
|
+
instrumented_text = ''
|
798
|
+
|
799
|
+
@cancel_instrumentation = false
|
800
|
+
num_lines = text.num_lines.to_f
|
801
|
+
num_lines = 1 if num_lines < 1
|
802
|
+
instrumented_text =
|
803
|
+
instrument_script_implementation(ruby_lex_utils,
|
804
|
+
text,
|
805
|
+
num_lines,
|
806
|
+
filename,
|
807
|
+
mark_private,
|
808
|
+
line_offset)
|
809
|
+
|
810
|
+
raise OpenC3::StopScript if @cancel_instrumentation
|
811
|
+
instrumented_text
|
812
|
+
end
|
813
|
+
|
814
|
+
def self.instrument_script_implementation(ruby_lex_utils,
|
815
|
+
text,
|
816
|
+
_num_lines,
|
817
|
+
filename,
|
818
|
+
mark_private = false,
|
819
|
+
line_offset = 0)
|
820
|
+
if mark_private
|
821
|
+
instrumented_text = 'private; '
|
822
|
+
else
|
823
|
+
instrumented_text = ''
|
824
|
+
end
|
825
|
+
|
826
|
+
ruby_lex_utils.each_lexed_segment(text) do |segment, instrumentable, inside_begin, line_no|
|
827
|
+
return nil if @cancel_instrumentation
|
828
|
+
instrumented_line = ''
|
829
|
+
if instrumentable
|
830
|
+
# Add a newline if it's empty to ensure the instrumented code has
|
831
|
+
# the same number of lines as the original script
|
832
|
+
if segment.strip.empty?
|
833
|
+
instrumented_text << "\n"
|
834
|
+
next
|
835
|
+
end
|
836
|
+
|
837
|
+
# Create a variable to hold the segment's return value
|
838
|
+
instrumented_line << "__return_val = nil; "
|
839
|
+
|
840
|
+
# If not inside a begin block then create one to catch exceptions
|
841
|
+
unless inside_begin
|
842
|
+
instrumented_line << 'begin; '
|
843
|
+
end
|
844
|
+
|
845
|
+
# Add preline instrumentation
|
846
|
+
instrumented_line << "RunningScript.instance.script_binding = binding(); "\
|
847
|
+
"RunningScript.instance.pre_line_instrumentation('#{filename}', #{line_no + line_offset}); "
|
848
|
+
|
849
|
+
# Add the actual line
|
850
|
+
instrumented_line << "__return_val = begin; "
|
851
|
+
instrumented_line << segment
|
852
|
+
instrumented_line.chomp!
|
853
|
+
|
854
|
+
# Add postline instrumentation
|
855
|
+
instrumented_line << " end; RunningScript.instance.post_line_instrumentation('#{filename}', #{line_no + line_offset}); "
|
856
|
+
|
857
|
+
# Complete begin block to catch exceptions
|
858
|
+
unless inside_begin
|
859
|
+
instrumented_line << "rescue Exception => eval_error; "\
|
860
|
+
"retry if RunningScript.instance.exception_instrumentation(eval_error, '#{filename}', #{line_no + line_offset}); end; "
|
861
|
+
end
|
862
|
+
|
863
|
+
instrumented_line << " __return_val\n"
|
864
|
+
else
|
865
|
+
unless segment =~ /^\s*end\s*$/ or segment =~ /^\s*when .*$/
|
866
|
+
num_left_brackets = segment.count('{')
|
867
|
+
num_right_brackets = segment.count('}')
|
868
|
+
num_left_square_brackets = segment.count('[')
|
869
|
+
num_right_square_brackets = segment.count(']')
|
870
|
+
|
871
|
+
if (num_right_brackets > num_left_brackets) ||
|
872
|
+
(num_right_square_brackets > num_left_square_brackets)
|
873
|
+
instrumented_line = segment
|
874
|
+
else
|
875
|
+
instrumented_line = "RunningScript.instance.pre_line_instrumentation('#{filename}', #{line_no + line_offset}); " + segment
|
876
|
+
end
|
877
|
+
else
|
878
|
+
instrumented_line = segment
|
879
|
+
end
|
880
|
+
end
|
881
|
+
|
882
|
+
instrumented_text << instrumented_line
|
883
|
+
end
|
884
|
+
instrumented_text
|
885
|
+
end
|
886
|
+
|
887
|
+
def pre_line_instrumentation(filename, line_number)
|
888
|
+
@pre_line_time = Time.now.sys
|
889
|
+
@script_status.current_filename = filename
|
890
|
+
@script_status.line_no = line_number
|
891
|
+
if @use_instrumentation
|
892
|
+
# Clear go
|
893
|
+
@go = false
|
894
|
+
|
895
|
+
# Handle stopping mid-script if necessary
|
896
|
+
raise OpenC3::StopScript if @stop
|
897
|
+
|
898
|
+
handle_potential_tab_change(filename)
|
899
|
+
|
900
|
+
# Adjust line number for offset in main script
|
901
|
+
line_number = line_number + @line_offset
|
902
|
+
detail_string = nil
|
903
|
+
if filename
|
904
|
+
detail_string = File.basename(filename) << ':' << line_number.to_s
|
905
|
+
OpenC3::Logger.detail_string = detail_string
|
906
|
+
end
|
907
|
+
|
908
|
+
update_running_script_store("running")
|
909
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
910
|
+
handle_pause(filename, line_number)
|
911
|
+
handle_line_delay()
|
912
|
+
end
|
913
|
+
end
|
914
|
+
|
915
|
+
def post_line_instrumentation(filename, line_number)
|
916
|
+
if @use_instrumentation
|
917
|
+
line_number = line_number + @line_offset
|
918
|
+
handle_output_io(filename, line_number)
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
def exception_instrumentation(error, filename, line_number)
|
923
|
+
if error.class <= OpenC3::StopScript || error.class <= OpenC3::SkipScript || !@use_instrumentation
|
924
|
+
raise error
|
925
|
+
elsif !error.eql?(@@error)
|
926
|
+
line_number = line_number + @line_offset
|
927
|
+
handle_exception(error, false, filename, line_number)
|
928
|
+
end
|
929
|
+
end
|
930
|
+
|
931
|
+
def perform_wait(prompt)
|
932
|
+
mark_waiting()
|
933
|
+
wait_for_go_or_stop(prompt: prompt)
|
934
|
+
end
|
935
|
+
|
936
|
+
def perform_pause
|
937
|
+
mark_paused()
|
938
|
+
wait_for_go_or_stop()
|
939
|
+
end
|
940
|
+
|
941
|
+
def perform_breakpoint(filename, line_number)
|
942
|
+
mark_breakpoint()
|
943
|
+
scriptrunner_puts "Hit Breakpoint at #{filename}:#{line_number}"
|
944
|
+
handle_output_io(filename, line_number)
|
945
|
+
wait_for_go_or_stop()
|
946
|
+
end
|
947
|
+
|
948
|
+
def debug(debug_text)
|
949
|
+
handle_output_io()
|
950
|
+
|
951
|
+
use_script_engine = false
|
952
|
+
extension = File.extname(current_filename()).to_s.downcase
|
953
|
+
if @script_engine and extension != ".py"
|
954
|
+
use_script_engine = true
|
955
|
+
end
|
956
|
+
|
957
|
+
if not use_script_engine
|
958
|
+
if @script_binding
|
959
|
+
# Check for accessing an instance variable or local
|
960
|
+
if debug_text =~ /^@\S+$/ || @script_binding.local_variables.include?(debug_text.to_sym)
|
961
|
+
debug_text = "puts #{debug_text}" # Automatically add puts to print it
|
962
|
+
end
|
963
|
+
eval(debug_text, @script_binding, 'debug', 1)
|
964
|
+
else
|
965
|
+
Object.class_eval(debug_text, 'debug', 1)
|
966
|
+
end
|
967
|
+
else
|
968
|
+
@script_engine.debug(debug_text)
|
969
|
+
end
|
970
|
+
|
971
|
+
handle_output_io()
|
972
|
+
rescue Exception => e
|
973
|
+
if e.class == DRb::DRbConnError
|
974
|
+
OpenC3::Logger.error("Error Connecting to Command and Telemetry Server")
|
975
|
+
else
|
976
|
+
OpenC3::Logger.error(e.formatted)
|
977
|
+
end
|
978
|
+
handle_output_io()
|
979
|
+
end
|
980
|
+
|
981
|
+
def self.set_breakpoint(filename, line_number)
|
982
|
+
@@breakpoints[filename] ||= {}
|
983
|
+
@@breakpoints[filename][line_number] = true
|
984
|
+
end
|
985
|
+
|
986
|
+
def self.clear_breakpoint(filename, line_number)
|
987
|
+
@@breakpoints[filename] ||= {}
|
988
|
+
@@breakpoints[filename].delete(line_number) if @@breakpoints[filename][line_number]
|
989
|
+
end
|
990
|
+
|
991
|
+
def self.clear_breakpoints(filename = nil)
|
992
|
+
if filename == nil or filename.empty?
|
993
|
+
@@breakpoints = {}
|
994
|
+
else
|
995
|
+
@@breakpoints.delete(filename)
|
996
|
+
end
|
997
|
+
end
|
998
|
+
|
999
|
+
def clear_breakpoints
|
1000
|
+
ScriptRunnerFrame.clear_breakpoints(unique_filename())
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
def current_backtrace
|
1004
|
+
trace = []
|
1005
|
+
if @@run_thread
|
1006
|
+
temp_trace = @@run_thread.backtrace
|
1007
|
+
temp_trace.each do |line|
|
1008
|
+
next if line.include?(OpenC3::PATH) # Ignore OpenC3 internals
|
1009
|
+
next if line.include?('lib/ruby/gems') # Ignore system gems
|
1010
|
+
next if line.include?('app/models/running_script') # Ignore this file
|
1011
|
+
trace << line
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
trace
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
def execute_while_paused(filename, line_no = 1, end_line_no = nil)
|
1018
|
+
if @script_status.state == 'paused' or @script_status.state == 'error' or @script_status.state == 'breakpoint'
|
1019
|
+
@execute_while_paused_info = { filename: filename, line_no: line_no, end_line_no: end_line_no }
|
1020
|
+
else
|
1021
|
+
scriptrunner_puts("Cannot execute selection or goto unless script is paused, breakpoint, or in error state")
|
1022
|
+
end
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
def scriptrunner_puts(string, color = 'BLACK')
|
1026
|
+
line_to_write = Time.now.sys.formatted + " (SCRIPTRUNNER): " + string
|
1027
|
+
$stdout.puts line_to_write
|
1028
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :output, line: line_to_write, color: color })
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
def handle_output_io(filename = nil, line_number = nil)
|
1032
|
+
filename = @script_status.current_filename if filename.nil?
|
1033
|
+
line_number = @script_status.line_no if line_number.nil?
|
1034
|
+
|
1035
|
+
@output_time = Time.now.sys
|
1036
|
+
if @output_io.string[-1..-1] == "\n"
|
1037
|
+
time_formatted = Time.now.sys.formatted
|
1038
|
+
color = 'BLACK'
|
1039
|
+
lines_to_write = ''
|
1040
|
+
out_line_number = line_number.to_s
|
1041
|
+
out_filename = File.basename(filename) if filename
|
1042
|
+
|
1043
|
+
# Build each line to write
|
1044
|
+
string = @output_io.string.clone
|
1045
|
+
@output_io.string = @output_io.string[string.length..-1]
|
1046
|
+
line_count = 0
|
1047
|
+
string.each_line(chomp: true) do |out_line|
|
1048
|
+
begin
|
1049
|
+
json = JSON.parse(out_line, :allow_nan => true, :create_additions => true)
|
1050
|
+
time_formatted = Time.parse(json["@timestamp"]).sys.formatted if json["@timestamp"]
|
1051
|
+
if json["log"]
|
1052
|
+
out_line = json["log"]
|
1053
|
+
elsif json["message"]
|
1054
|
+
out_line = json["message"]
|
1055
|
+
end
|
1056
|
+
rescue
|
1057
|
+
# Regular output
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
if out_line.length >= 25 and out_line[0..1] == '20' and out_line[10] == ' ' and out_line[23..24] == ' ('
|
1061
|
+
line_to_write = out_line
|
1062
|
+
else
|
1063
|
+
if filename
|
1064
|
+
line_to_write = time_formatted + " (#{out_filename}:#{out_line_number}): " + out_line
|
1065
|
+
else
|
1066
|
+
line_to_write = time_formatted + " (SCRIPTRUNNER): " + out_line
|
1067
|
+
color = 'BLUE'
|
1068
|
+
end
|
1069
|
+
end
|
1070
|
+
lines_to_write << (line_to_write + "\n")
|
1071
|
+
line_count += 1
|
1072
|
+
end # string.each_line
|
1073
|
+
|
1074
|
+
if lines_to_write.length > @@max_output_characters
|
1075
|
+
# We want the full @@max_output_characters so don't subtract the additional "ERROR: ..." text
|
1076
|
+
published_lines = lines_to_write[0...@@max_output_characters]
|
1077
|
+
published_lines << "\nERROR: Too much to publish. Truncating #{lines_to_write.length} characters of output to #{@@max_output_characters} characters.\n"
|
1078
|
+
else
|
1079
|
+
published_lines = lines_to_write
|
1080
|
+
end
|
1081
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :output, line: published_lines.as_json(:allow_nan => true), color: color })
|
1082
|
+
# Add to the message log
|
1083
|
+
message_log.write(lines_to_write)
|
1084
|
+
end
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
def graceful_kill
|
1088
|
+
# Just to avoid warning
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
def wait_for_go_or_stop(error = nil, prompt: nil)
|
1092
|
+
count = -1
|
1093
|
+
@go = false
|
1094
|
+
@prompt_id = prompt['id'] if prompt
|
1095
|
+
until (@go or @stop)
|
1096
|
+
check_execute_while_paused()
|
1097
|
+
sleep(0.01)
|
1098
|
+
count += 1
|
1099
|
+
if count % 100 == 0 # Approximately Every Second
|
1100
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1101
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :script, method: prompt['method'], prompt_id: prompt['id'], args: prompt['args'], kwargs: prompt['kwargs'] }) if prompt
|
1102
|
+
end
|
1103
|
+
end
|
1104
|
+
clear_prompt() if prompt
|
1105
|
+
RunningScript.instance.prompt_id = nil
|
1106
|
+
@go = false
|
1107
|
+
mark_running()
|
1108
|
+
raise OpenC3::StopScript if @stop
|
1109
|
+
raise error if error and !@continue_after_error
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
def wait_for_go_or_stop_or_retry(error = nil)
|
1113
|
+
count = 0
|
1114
|
+
@go = false
|
1115
|
+
until (@go or @stop or @retry_needed)
|
1116
|
+
check_execute_while_paused()
|
1117
|
+
sleep(0.01)
|
1118
|
+
count += 1
|
1119
|
+
if (count % 100) == 0 # Approximately Every Second
|
1120
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1121
|
+
end
|
1122
|
+
end
|
1123
|
+
@go = false
|
1124
|
+
mark_running()
|
1125
|
+
raise OpenC3::StopScript if @stop
|
1126
|
+
raise error if error and !@continue_after_error
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
def check_execute_while_paused
|
1130
|
+
if @execute_while_paused_info
|
1131
|
+
if @script_status.current_filename == @execute_while_paused_info[:filename]
|
1132
|
+
bind_variables = true
|
1133
|
+
else
|
1134
|
+
bind_variables = false
|
1135
|
+
end
|
1136
|
+
if @execute_while_paused_info[:end_line_no]
|
1137
|
+
# Execute Selection While Paused
|
1138
|
+
state = @script_status.state
|
1139
|
+
current_filename = @script_status.current_filename
|
1140
|
+
line_no = @script_status.line_no
|
1141
|
+
start(@execute_while_paused_info[:filename], line_no: @execute_while_paused_info[:line_no], end_line_no: @execute_while_paused_info[:end_line_no], bind_variables: bind_variables)
|
1142
|
+
# Need to restore state after returning so that the correct line will be shown in ScriptRunner
|
1143
|
+
@script_status.state = state
|
1144
|
+
@script_status.current_filename = current_filename
|
1145
|
+
@script_status.line_no = line_no
|
1146
|
+
@script_status.update(queued: true)
|
1147
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1148
|
+
else
|
1149
|
+
# Goto While Paused
|
1150
|
+
start(@execute_while_paused_info[:filename], line_no: @execute_while_paused_info[:line_no], bind_variables: bind_variables, complete: true)
|
1151
|
+
end
|
1152
|
+
end
|
1153
|
+
ensure
|
1154
|
+
@execute_while_paused_info = nil
|
1155
|
+
end
|
1156
|
+
|
1157
|
+
def mark_running
|
1158
|
+
update_running_script_store("running")
|
1159
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1160
|
+
end
|
1161
|
+
|
1162
|
+
def mark_paused
|
1163
|
+
update_running_script_store("paused")
|
1164
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
def mark_waiting
|
1168
|
+
update_running_script_store("waiting")
|
1169
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
def mark_error
|
1173
|
+
update_running_script_store("error")
|
1174
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1175
|
+
end
|
1176
|
+
|
1177
|
+
def mark_crashed
|
1178
|
+
@script_status.end_time = Time.now.utc.iso8601
|
1179
|
+
update_running_script_store("crashed")
|
1180
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
def mark_completed
|
1184
|
+
@script_status.end_time = Time.now.utc.iso8601
|
1185
|
+
update_running_script_store("completed")
|
1186
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1187
|
+
if OpenC3::SuiteRunner.suite_results
|
1188
|
+
OpenC3::SuiteRunner.suite_results.complete
|
1189
|
+
# context looks like the following:
|
1190
|
+
# MySuite:ExampleGroup:script_2
|
1191
|
+
# MySuite:ExampleGroup Manual Setup
|
1192
|
+
# MySuite Manual Teardown
|
1193
|
+
init_split = OpenC3::SuiteRunner.suite_results.context.split()
|
1194
|
+
parts = init_split[0].split(':')
|
1195
|
+
if parts[2]
|
1196
|
+
# Remove test_ or script_ because it doesn't add any info
|
1197
|
+
parts[2] = parts[2].sub(/^test_/, '').sub(/^script_/, '')
|
1198
|
+
end
|
1199
|
+
parts.map! { |part| part[0..9] } # Only take the first 10 characters to prevent huge filenames
|
1200
|
+
# If the initial split on whitespace has more than 1 item it means
|
1201
|
+
# a Manual Setup or Teardown was performed. Add this to the filename.
|
1202
|
+
# NOTE: We're doing this here with a single underscore to preserve
|
1203
|
+
# double underscores as Suite, Group, Script delimiters
|
1204
|
+
if parts[1] and init_split.length > 1
|
1205
|
+
parts[1] += "_#{init_split[-1]}"
|
1206
|
+
elsif parts[0] and init_split.length > 1
|
1207
|
+
parts[0] += "_#{init_split[-1]}"
|
1208
|
+
end
|
1209
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :report, report: OpenC3::SuiteRunner.suite_results.report })
|
1210
|
+
# Write out the report to a local file
|
1211
|
+
log_dir = File.join(RAILS_ROOT, 'log')
|
1212
|
+
filename = File.join(log_dir, File.build_timestamped_filename(['sr', parts.join('__')]))
|
1213
|
+
File.open(filename, 'wb') do |file|
|
1214
|
+
file.write(OpenC3::SuiteRunner.suite_results.report)
|
1215
|
+
end
|
1216
|
+
# Generate the bucket key by removing the date underscores in the filename to create the bucket file structure
|
1217
|
+
bucket_key = File.join("#{@script_status.scope}/tool_logs/sr/", File.basename(filename)[0..9].gsub("_", ""), File.basename(filename))
|
1218
|
+
metadata = {
|
1219
|
+
# Note: The chars '(' and ')' are used by RunningScripts.vue to differentiate between script logs
|
1220
|
+
"id" => @script_status.id,
|
1221
|
+
"user" => @script_status.username,
|
1222
|
+
"scriptname" => "#{@script_status.current_filename} (#{OpenC3::SuiteRunner.suite_results.context.strip})"
|
1223
|
+
}
|
1224
|
+
thread = OpenC3::BucketUtilities.move_log_file_to_bucket(filename, bucket_key, metadata: metadata)
|
1225
|
+
# Wait for the file to get moved to S3 because after this the process will likely die
|
1226
|
+
@script_status.report = bucket_key
|
1227
|
+
@script_status.update(queued: true)
|
1228
|
+
thread.join
|
1229
|
+
end
|
1230
|
+
running_script_publish("cmd-running-script-channel:#{@script_status.id}", "shutdown")
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
def mark_breakpoint
|
1234
|
+
update_running_script_store("breakpoint")
|
1235
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :line, filename: @script_status.current_filename, line_no: @script_status.line_no, state: @script_status.state })
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
def run_text(text,
|
1239
|
+
initial_filename: nil)
|
1240
|
+
initialize_variables()
|
1241
|
+
saved_instance = @@instance
|
1242
|
+
saved_run_thread = @@run_thread
|
1243
|
+
@@instance = self
|
1244
|
+
|
1245
|
+
@@run_thread = Thread.new do
|
1246
|
+
begin
|
1247
|
+
# Capture STDOUT and STDERR
|
1248
|
+
$stdout.add_stream(@output_io)
|
1249
|
+
$stderr.add_stream(@output_io)
|
1250
|
+
|
1251
|
+
output = "Starting script: #{File.basename(@script_status.filename)}"
|
1252
|
+
output += " in DISCONNECT mode" if $disconnect
|
1253
|
+
output += ", line_delay = #{@@line_delay}"
|
1254
|
+
scriptrunner_puts(output)
|
1255
|
+
handle_output_io()
|
1256
|
+
|
1257
|
+
# Start Output Thread
|
1258
|
+
@@output_thread = Thread.new { output_thread() } unless @@output_thread
|
1259
|
+
|
1260
|
+
if @script_engine
|
1261
|
+
if @script_status.start_line_no != 1 or !@script_status.end_line_no.nil?
|
1262
|
+
if @script_status.end_line_no.nil?
|
1263
|
+
# Goto line
|
1264
|
+
start(@script_status.filename, line_no: @script_status.start_line_no, complete: true)
|
1265
|
+
else
|
1266
|
+
# Execute selection
|
1267
|
+
start(@script_status.filename, line_no: @script_status.start_line_no, end_line_no: @script_status.end_line_no, complete: true)
|
1268
|
+
end
|
1269
|
+
else
|
1270
|
+
@script_engine.run_text(text, filename: @script_status.filename)
|
1271
|
+
end
|
1272
|
+
else
|
1273
|
+
if initial_filename == 'SCRIPTRUNNER'
|
1274
|
+
# Don't instrument pseudo scripts
|
1275
|
+
instrument_filename = initial_filename
|
1276
|
+
instrumented_script = text
|
1277
|
+
else
|
1278
|
+
# Instrument everything else
|
1279
|
+
instrument_filename = @script_status.filename
|
1280
|
+
instrument_filename = initial_filename if initial_filename
|
1281
|
+
instrumented_script = self.class.instrument_script(text, instrument_filename, true)
|
1282
|
+
end
|
1283
|
+
|
1284
|
+
# Execute the script with warnings disabled
|
1285
|
+
OpenC3.disable_warnings do
|
1286
|
+
@pre_line_time = Time.now.sys
|
1287
|
+
Object.class_eval(instrumented_script, instrument_filename, 1)
|
1288
|
+
end
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
handle_output_io()
|
1292
|
+
scriptrunner_puts "Script completed: #{@script_status.filename}"
|
1293
|
+
|
1294
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
1295
|
+
if e.class <= OpenC3::StopScript or e.class <= OpenC3::SkipScript
|
1296
|
+
handle_output_io()
|
1297
|
+
scriptrunner_puts "Script stopped: #{@script_status.filename}"
|
1298
|
+
else
|
1299
|
+
filename, line_number = e.source
|
1300
|
+
handle_exception(e, true, filename, line_number)
|
1301
|
+
handle_output_io()
|
1302
|
+
scriptrunner_puts "Exception in Control Statement - Script stopped: #{@script_status.filename}"
|
1303
|
+
mark_crashed()
|
1304
|
+
end
|
1305
|
+
ensure
|
1306
|
+
# Stop Capturing STDOUT and STDERR
|
1307
|
+
# Check for remove_stream because if the tool is quitting the
|
1308
|
+
# OpenC3::restore_io may have been called which sets $stdout and
|
1309
|
+
# $stderr to the IO constant
|
1310
|
+
$stdout.remove_stream(@output_io) if $stdout.respond_to? :remove_stream
|
1311
|
+
$stderr.remove_stream(@output_io) if $stderr.respond_to? :remove_stream
|
1312
|
+
|
1313
|
+
# Clear run thread and instance to indicate we are no longer running
|
1314
|
+
@@instance = saved_instance
|
1315
|
+
@@run_thread = saved_run_thread
|
1316
|
+
@active_script = @script
|
1317
|
+
@script_binding = nil
|
1318
|
+
# Set the current_filename to the original file and the line_no to 0
|
1319
|
+
# so the mark_complete method will signal the frontend to reset to the original
|
1320
|
+
@script_status.current_filename = @script_status.filename
|
1321
|
+
@script_status.line_no = 0
|
1322
|
+
if @@output_thread and not @@instance
|
1323
|
+
@@cancel_output = true
|
1324
|
+
@@output_sleeper.cancel
|
1325
|
+
OpenC3.kill_thread(self, @@output_thread)
|
1326
|
+
@@output_thread = nil
|
1327
|
+
end
|
1328
|
+
mark_completed()
|
1329
|
+
end
|
1330
|
+
end
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
def handle_potential_tab_change(filename)
|
1334
|
+
# Make sure the correct file is shown in script runner
|
1335
|
+
if @current_file != filename
|
1336
|
+
if @call_stack.include?(filename)
|
1337
|
+
index = @call_stack.index(filename)
|
1338
|
+
else # new file
|
1339
|
+
@call_stack.push(filename.dup)
|
1340
|
+
load_file_into_script(filename)
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
@current_file = filename
|
1344
|
+
end
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
def handle_pause(filename, line_number)
|
1348
|
+
breakpoint = false
|
1349
|
+
breakpoint = true if @@breakpoints[filename] and @@breakpoints[filename][line_number]
|
1350
|
+
|
1351
|
+
filename = File.basename(filename)
|
1352
|
+
if @pause
|
1353
|
+
@pause = false unless @step
|
1354
|
+
if breakpoint
|
1355
|
+
perform_breakpoint(filename, line_number)
|
1356
|
+
else
|
1357
|
+
perform_pause()
|
1358
|
+
end
|
1359
|
+
else
|
1360
|
+
perform_breakpoint(filename, line_number) if breakpoint
|
1361
|
+
end
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
def handle_line_delay
|
1365
|
+
if @@line_delay > 0.0
|
1366
|
+
sleep_time = @@line_delay - (Time.now.sys - @pre_line_time)
|
1367
|
+
sleep(sleep_time) if sleep_time > 0.0
|
1368
|
+
end
|
1369
|
+
end
|
1370
|
+
|
1371
|
+
def handle_exception(error, fatal, filename = nil, line_number = 0)
|
1372
|
+
@exceptions ||= []
|
1373
|
+
@exceptions << error
|
1374
|
+
@script_status.errors ||= []
|
1375
|
+
@script_status.errors << error.formatted
|
1376
|
+
@@error = error
|
1377
|
+
|
1378
|
+
if error.class == DRb::DRbConnError
|
1379
|
+
OpenC3::Logger.error("Error Connecting to Command and Telemetry Server")
|
1380
|
+
elsif error.class == OpenC3::CheckError
|
1381
|
+
OpenC3::Logger.error(error.message)
|
1382
|
+
else
|
1383
|
+
OpenC3::Logger.error(error.class.to_s.split('::')[-1] + ' : ' + error.message)
|
1384
|
+
if ENV['OPENC3_FULL_BACKTRACE']
|
1385
|
+
OpenC3::Logger.error(error.backtrace.join("\n\n"))
|
1386
|
+
end
|
1387
|
+
end
|
1388
|
+
handle_output_io(filename, line_number)
|
1389
|
+
|
1390
|
+
raise error if !@@pause_on_error and !@continue_after_error and !fatal
|
1391
|
+
|
1392
|
+
if !fatal and @@pause_on_error
|
1393
|
+
mark_error()
|
1394
|
+
wait_for_go_or_stop_or_retry(error)
|
1395
|
+
end
|
1396
|
+
|
1397
|
+
if @retry_needed
|
1398
|
+
@retry_needed = false
|
1399
|
+
true
|
1400
|
+
else
|
1401
|
+
false
|
1402
|
+
end
|
1403
|
+
end
|
1404
|
+
|
1405
|
+
def load_file_into_script(filename)
|
1406
|
+
mark_breakpoints(filename)
|
1407
|
+
breakpoints = @@breakpoints[filename]&.filter { |_, present| present }&.map { |line_number, _| line_number - 1 } # -1 because frontend lines are 0-indexed
|
1408
|
+
breakpoints ||= []
|
1409
|
+
cached = @@file_cache[filename]
|
1410
|
+
if cached
|
1411
|
+
@body = cached
|
1412
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :file, filename: filename, text: @body.to_utf8, breakpoints: breakpoints })
|
1413
|
+
else
|
1414
|
+
text = ::Script.body(@script_status.scope, filename)
|
1415
|
+
raise "Script not found: #{filename}" if text.nil?
|
1416
|
+
@@file_cache[filename] = text
|
1417
|
+
@body = text
|
1418
|
+
running_script_anycable_publish("running-script-channel:#{@script_status.id}", { type: :file, filename: filename, text: @body.to_utf8, breakpoints: breakpoints })
|
1419
|
+
end
|
1420
|
+
end
|
1421
|
+
|
1422
|
+
def mark_breakpoints(filename)
|
1423
|
+
breakpoints = @@breakpoints[filename]
|
1424
|
+
if breakpoints
|
1425
|
+
breakpoints.each do |line_number, present|
|
1426
|
+
RunningScript.set_breakpoint(filename, line_number) if present
|
1427
|
+
end
|
1428
|
+
else
|
1429
|
+
::Script.get_breakpoints(@script_status.scope, filename).each do |line_number|
|
1430
|
+
RunningScript.set_breakpoint(filename, line_number + 1)
|
1431
|
+
end
|
1432
|
+
end
|
1433
|
+
end
|
1434
|
+
|
1435
|
+
def redirect_io
|
1436
|
+
# Redirect Standard Output and Standard Error
|
1437
|
+
$stdout = OpenC3::Stdout.instance
|
1438
|
+
$stderr = OpenC3::Stderr.instance
|
1439
|
+
OpenC3::Logger.stdout = true
|
1440
|
+
OpenC3::Logger.level = OpenC3::Logger::INFO
|
1441
|
+
end
|
1442
|
+
|
1443
|
+
def output_thread
|
1444
|
+
@@cancel_output = false
|
1445
|
+
@@output_sleeper = OpenC3::Sleeper.new
|
1446
|
+
begin
|
1447
|
+
loop do
|
1448
|
+
break if @@cancel_output
|
1449
|
+
handle_output_io() if (Time.now.sys - @output_time) > 5.0
|
1450
|
+
break if @@cancel_output
|
1451
|
+
break if @@output_sleeper.sleep(1.0)
|
1452
|
+
end # loop
|
1453
|
+
rescue => e
|
1454
|
+
# Qt.execute_in_main_thread(true) do
|
1455
|
+
# ExceptionDialog.new(self, error, "Output Thread")
|
1456
|
+
# end
|
1457
|
+
end
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
end
|