rpremote 0.1.0 → 0.3.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.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "net/http"
5
+ require "timeout"
6
+ require "uri"
7
+
8
+ module Rpremote
9
+ class NukeFirmware
10
+ URL = "https://github.com/raspberrypi/pico-sdk-prebuilts/releases/latest/download/nuke_universal.uf2"
11
+ FILENAME = "nuke_universal.uf2"
12
+ MAX_REDIRECTS = 5
13
+
14
+ class DownloadError < Rpremote::Error; end
15
+
16
+ def initialize(directory: Target::DEFAULT_CACHE_DIR, fetcher: nil)
17
+ @directory = File.expand_path(directory)
18
+ @fetcher = fetcher || method(:download)
19
+ end
20
+
21
+ def setup(force: false)
22
+ FileUtils.mkdir_p(directory)
23
+ return path if File.size?(path) && !force
24
+
25
+ temporary = "#{path}.part"
26
+ File.binwrite(temporary, fetcher.call(URL))
27
+ raise DownloadError, "downloaded nuke_universal.uf2 is empty" unless File.size?(temporary)
28
+
29
+ File.rename(temporary, path)
30
+ path
31
+ rescue SystemCallError => e
32
+ raise DownloadError, "cannot store nuke_universal.uf2: #{e.message}"
33
+ ensure
34
+ FileUtils.rm_f(temporary) if temporary
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :directory, :fetcher
40
+
41
+ def path
42
+ File.join(directory, FILENAME)
43
+ end
44
+
45
+ def download(url, redirects = MAX_REDIRECTS)
46
+ raise DownloadError, "too many redirects while downloading nuke_universal.uf2" if redirects.negative?
47
+
48
+ uri = URI(url)
49
+ raise DownloadError, "nuke_universal.uf2 download requires HTTPS" unless uri.is_a?(URI::HTTPS)
50
+
51
+ request = Net::HTTP::Get.new(uri)
52
+ request["User-Agent"] = "rpremote/#{Rpremote::VERSION}"
53
+ response = Net::HTTP.start(
54
+ uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 60
55
+ ) { |http| http.request(request) }
56
+
57
+ case response
58
+ when Net::HTTPSuccess then response.body
59
+ when Net::HTTPRedirection
60
+ location = response["location"]
61
+ raise DownloadError, "nuke_universal.uf2 download redirect has no location" unless location
62
+
63
+ download(URI.join(url, location).to_s, redirects - 1)
64
+ else
65
+ raise DownloadError, "nuke_universal.uf2 download failed: HTTP #{response.code} #{response.message}"
66
+ end
67
+ rescue SocketError, SystemCallError, Timeout::Error => e
68
+ raise DownloadError, "nuke_universal.uf2 download failed: #{e.message}"
69
+ end
70
+ end
71
+ end
@@ -24,7 +24,7 @@ module Rpremote
24
24
  READY = 0x01
25
25
 
26
26
  CHUNK_SIZE = 480
27
- DEFAULT_TIMEOUT = 10.0
27
+ DEFAULT_TIMEOUT = 20.0
28
28
  MAX_FRAME_BODY = 65_535
29
29
  DFU_MAGIC = "DFU\0".b
30
30
  DFU_VERSION = 1
@@ -87,9 +87,7 @@ module Rpremote
87
87
  when DONE_ACK
88
88
  status, remote_crc = parse_completion(payload)
89
89
  raise DeviceError, format("download failed with status 0x%02x", status) unless status == OK
90
- if total && data.bytesize != total
91
- raise ProtocolError, "file size mismatch: expected #{total}, received #{data.bytesize}"
92
- end
90
+ raise ProtocolError, "file size mismatch: expected #{total}, received #{data.bytesize}" if total && data.bytesize != total
93
91
 
94
92
  local_crc = Checksum.crc32(data)
95
93
  raise ChecksumError, checksum_mismatch("CRC32", local_crc, remote_crc, 8) unless remote_crc == local_crc
@@ -194,9 +192,7 @@ module Rpremote
194
192
  def expect_status(expected_command, expected_status, context)
195
193
  command, payload = receive_frame
196
194
  raise DeviceError, payload.force_encoding(Encoding::UTF_8) if command == ERROR
197
- unless command == expected_command
198
- raise ProtocolError, "unexpected response #{hex(command, 2)} while waiting for #{context}"
199
- end
195
+ raise ProtocolError, "unexpected response #{hex(command, 2)} while waiting for #{context}" unless command == expected_command
200
196
  raise ProtocolError, "#{context} has no status" if payload.empty?
201
197
  return if payload.getbyte(0) == expected_status
202
198
 
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ class PicoRubySourcePatch
5
+ JOB_PATH = "mrbgems/picoruby-shell/mrblib/job.rb"
6
+ BOOTSEL_PATH = "mrbgems/picoruby-machine/include/machine.h"
7
+ PATCH_ALIASES = { "3.4.2" => "3.4.5" }.freeze
8
+
9
+ class Error < Rpremote::Error; end
10
+
11
+ attr_reader :version
12
+
13
+ def initialize(version:)
14
+ @version = version
15
+ end
16
+
17
+ def apply(source)
18
+ return unless Dir.exist?(source)
19
+
20
+ patch_version = PATCH_ALIASES.fetch(version, version)
21
+ patches(patch_version).each do |patch, prerequisite|
22
+ next unless File.file?(patch) && File.file?(File.join(source, prerequisite))
23
+ next if prerequisite == BOOTSEL_PATH && File.read(File.join(source, prerequisite)).include?("Machine_bootsel")
24
+
25
+ next if git_apply?(source, patch, "--reverse", "--check")
26
+
27
+ raise Error, "cannot apply rpremote patch to PicoRuby #{version}" unless git_apply?(source, patch, "--check")
28
+ raise Error, "cannot apply rpremote patch to PicoRuby #{version}" unless git_apply?(source, patch)
29
+ end
30
+ nil
31
+ end
32
+
33
+ private
34
+
35
+ def patches(patch_version)
36
+ definitions = {
37
+ "ruby-exception-status" => JOB_PATH,
38
+ "bootsel" => BOOTSEL_PATH
39
+ }
40
+ definitions.map do |name, prerequisite|
41
+ [File.expand_path("../../patches/picoruby-#{patch_version}-#{name}.patch", __dir__), prerequisite]
42
+ end
43
+ end
44
+
45
+ def git_apply?(source, patch, *options)
46
+ system("git", "apply", "--unidiff-zero", *options, patch, chdir: source, out: File::NULL, err: File::NULL)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ class RecursiveCopy
5
+ def initialize(output:, serial: Serial, device: Device)
6
+ @output = output
7
+ @serial = serial
8
+ @device = device
9
+ end
10
+
11
+ def call(source, destination, options)
12
+ validate!(source, destination)
13
+ remote_root = RemotePath.validate(destination)
14
+ directories, files = collect(source, remote_root)
15
+ create_directories(directories, options)
16
+ upload_files(files, options)
17
+ output.puts("uploaded #{files.length} files: #{source} -> #{destination}")
18
+ rescue Errno::ENOENT => e
19
+ raise ArgumentError, e.message
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :output, :serial, :device
25
+
26
+ def validate!(source, destination)
27
+ raise ArgumentError, "recursive cp only supports a local source and remote destination" if RemotePath.remote?(source)
28
+ raise ArgumentError, "recursive cp source must be a directory: #{source}" unless File.directory?(source)
29
+
30
+ RemotePath.validate(destination)
31
+ end
32
+
33
+ def collect(source, remote_root)
34
+ directories = [remote_root]
35
+ files = []
36
+ local_entries(source).each do |path|
37
+ remote_path = File.join(remote_root, path.delete_prefix("#{source}/"))
38
+ File.directory?(path) ? directories << remote_path : files << [path, remote_path]
39
+ end
40
+ [directories.sort_by { |path| [path.count("/"), path] }.uniq, files.sort]
41
+ end
42
+
43
+ def local_entries(source)
44
+ entries = Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).reject do |path|
45
+ %w[. ..].include?(File.basename(path))
46
+ end
47
+ symbolic_link = entries.find { |path| File.symlink?(path) }
48
+ raise ArgumentError, "recursive cp does not support symbolic links: #{symbolic_link}" if symbolic_link
49
+
50
+ entries
51
+ end
52
+
53
+ def create_directories(directories, options)
54
+ with_port(options) do |port|
55
+ shell = Shell.new(port, timeout: options[:timeout])
56
+ shell.synchronize!
57
+ directories.each do |path|
58
+ quoted_path = Shell.quote_argument(path)
59
+ next unless shell.execute("ls #{quoted_path}").start_with?("ls:")
60
+
61
+ result = shell.execute("mkdir #{quoted_path}")
62
+ raise Shell::CommandError, result.strip unless result.empty?
63
+
64
+ output.puts("created remote directory: :#{path}")
65
+ end
66
+ end
67
+ end
68
+
69
+ def upload_files(files, options)
70
+ with_port(options) do |port|
71
+ modem = PicoModem.new(port, timeout: options[:timeout])
72
+ files.each do |local_path, remote_path|
73
+ size = modem.upload(remote_path, File.binread(local_path))
74
+ output.puts("uploaded #{size} bytes: #{local_path} -> :#{remote_path}")
75
+ end
76
+ end
77
+ end
78
+
79
+ def with_port(options, &)
80
+ port_path = device.main_port(options[:port])
81
+ serial.open(port_path, baud: options[:baud], &)
82
+ end
83
+ end
84
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rpremote
4
4
  class Resetter
5
- DEFAULT_TIMEOUT = 10.0
5
+ DEFAULT_TIMEOUT = 20.0
6
6
  RETRY_INTERVAL = 0.1
7
7
  PROBE_TIMEOUT = 1.0
8
8
 
@@ -3,6 +3,8 @@
3
3
  module Rpremote
4
4
  class Runner
5
5
  REMOTE_DIRECTORY = "/home"
6
+ RUN_REMOTE_PATH = "#{REMOTE_DIRECTORY}/.rpremote-run.rb".freeze
7
+ DEPLOY_REMOTE_PATH = "#{REMOTE_DIRECTORY}/.rpremote-deploy.rb".freeze
6
8
 
7
9
  attr_reader :io, :timeout
8
10
 
@@ -20,16 +22,33 @@ module Rpremote
20
22
  @path_factory = path_factory || method(:temporary_path)
21
23
  end
22
24
 
23
- def run(data)
24
- remote_path = @path_factory.call
25
+ def run(data, output: nil, cleanup: true, remote_path: nil, diagnostics: nil)
26
+ remote_path ||= @path_factory.call
25
27
  shell = @shell_class.new(io, timeout: timeout)
28
+ trace(diagnostics, "SYNC_BEFORE_UPLOAD_START")
26
29
  shell.synchronize!
30
+ trace(diagnostics, "SYNC_BEFORE_UPLOAD_DONE")
31
+ trace(diagnostics, "UPLOAD_START", "bytes=#{data.bytesize},path=#{remote_path}")
27
32
  @modem_class.new(io, timeout: timeout).upload(remote_path, data)
33
+ trace(diagnostics, "UPLOAD_DONE")
34
+ trace(diagnostics, "SYNC_BEFORE_RUN_START")
28
35
  shell.synchronize!
36
+ trace(diagnostics, "SYNC_BEFORE_RUN_DONE")
29
37
  shell_ready = true
30
- shell.execute("./#{File.basename(remote_path)}")
38
+ command = "./#{File.basename(remote_path)}"
39
+ trace(diagnostics, "EXECUTE_START", "command=#{command}")
40
+ result = output ? shell.execute(command, output: output, idle_timeout: true) : shell.execute(command, idle_timeout: true)
41
+ trace(diagnostics, "EXECUTE_DONE", "bytes=#{result.to_s.bytesize}")
42
+ result
43
+ rescue StandardError => e
44
+ trace(diagnostics, "ERROR", "class=#{e.class},message=#{e.message.to_s.gsub("\n", " ")}")
45
+ raise
31
46
  ensure
32
- cleanup(shell, remote_path) if shell_ready && remote_path
47
+ if cleanup && shell_ready && remote_path
48
+ trace(diagnostics, "CLEANUP_START", "path=#{remote_path}")
49
+ cleanup(shell, remote_path)
50
+ trace(diagnostics, "CLEANUP_DONE")
51
+ end
33
52
  end
34
53
 
35
54
  private
@@ -40,9 +59,17 @@ module Rpremote
40
59
  nil
41
60
  end
42
61
 
62
+ def trace(diagnostics, event, details = nil)
63
+ return unless diagnostics
64
+
65
+ line = "rpremote: run event=#{event}"
66
+ line += ",#{details}" if details
67
+ diagnostics.puts(line)
68
+ diagnostics.flush if diagnostics.respond_to?(:flush)
69
+ end
70
+
43
71
  def temporary_path
44
- stamp = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1_000_000).to_i
45
- "#{REMOTE_DIRECTORY}/.rpremote-run-#{Process.pid}-#{stamp}.rb"
72
+ RUN_REMOTE_PATH
46
73
  end
47
74
  end
48
75
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optparse"
4
+ require_relative "nuke_firmware"
4
5
 
5
6
  module Rpremote
6
7
  class SetupCommand
7
- def self.run(args, defaults:, config:, config_filename:, output: $stdout)
8
+ def self.run(args, defaults:, config:, config_filename:, output: $stdout, nuke_firmware: NukeFirmware)
8
9
  options = {
9
10
  force: false,
10
11
  cache_dir: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
@@ -29,6 +30,8 @@ module Rpremote
29
30
  cache_dir: target.cache_dir
30
31
  ).setup(force: options[:force])
31
32
  output.puts("installed #{target.language} #{target.language_version}: #{path}")
33
+ nuke_path = nuke_firmware.new.setup(force: options[:force])
34
+ output.puts("installed Raspberry Pi nuke firmware: #{nuke_path}")
32
35
  end
33
36
  end
34
37
  end
@@ -4,13 +4,14 @@ require "io/wait"
4
4
 
5
5
  module Rpremote
6
6
  class Shell
7
- DEFAULT_TIMEOUT = 10.0
7
+ DEFAULT_TIMEOUT = 20.0
8
8
  PROMPT_START = "\e[?25l\e[1G$> \e[0K".b
9
9
  PROMPT_END = "\e[?25h".b
10
10
  ECHO_START = "\e[?25l\e[1G$> ".b
11
11
  ERASE_LINE = "\e[0K".b
12
12
  CURSOR_POSITION_QUERY = "\e[6n".b
13
13
  CURSOR_POSITION_RESPONSE = "\e[1;1R".b
14
+ RUBY_EXCEPTION_STATUS = "\x1eR2P2:RUBY_EXCEPTION\x1f".b
14
15
 
15
16
  class Error < Rpremote::Error; end
16
17
  class TimeoutError < Error; end
@@ -28,11 +29,12 @@ module Rpremote
28
29
 
29
30
  attr_reader :io, :timeout
30
31
 
31
- def initialize(io, timeout: DEFAULT_TIMEOUT)
32
+ def initialize(io, timeout: DEFAULT_TIMEOUT, clock: nil)
32
33
  raise ArgumentError, "timeout must be positive" unless timeout.positive?
33
34
 
34
35
  @io = io
35
36
  @timeout = timeout
37
+ @clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
36
38
  end
37
39
 
38
40
  def synchronize!
@@ -42,11 +44,20 @@ module Rpremote
42
44
  nil
43
45
  end
44
46
 
45
- def execute(command)
47
+ def execute(command, output: nil, idle_timeout: false)
46
48
  command = validate_command(command)
47
49
  send_command(command)
48
- response = read_until_prompt(after: ECHO_START + command.b + ERASE_LINE)
49
- extract_output(response, command)
50
+ echo = ECHO_START + command.b + ERASE_LINE
51
+ stream_state = {}
52
+ response = read_until_prompt(after: echo, idle_timeout: idle_timeout) do |buffer|
53
+ stream_output(output, buffer, echo, stream_state)
54
+ end
55
+ command_output = extract_output(response, command)
56
+ ruby_exception = command_output.include?(RUBY_EXCEPTION_STATUS)
57
+ command_output = remove_ruby_exception_status(command_output)
58
+ raise CommandError, "Ruby exception reported by R2P2" if ruby_exception
59
+
60
+ command_output
50
61
  rescue TimeoutError
51
62
  interrupt
52
63
  raise
@@ -68,7 +79,7 @@ module Rpremote
68
79
  command
69
80
  end
70
81
 
71
- def read_until_prompt(wait: timeout, after: nil)
82
+ def read_until_prompt(wait: timeout, after: nil, idle_timeout: false)
72
83
  deadline = monotonic_time + wait
73
84
  buffer = +"".b
74
85
  loop do
@@ -77,7 +88,9 @@ module Rpremote
77
88
  return buffer if prompt_at && buffer.index(PROMPT_END, prompt_at + PROMPT_START.bytesize)
78
89
 
79
90
  buffer << read_available(deadline)
91
+ deadline = monotonic_time + wait if idle_timeout
80
92
  answer_terminal_queries(buffer)
93
+ yield buffer if block_given?
81
94
  end
82
95
  end
83
96
 
@@ -95,6 +108,30 @@ module Rpremote
95
108
  response.byteslice((output_at + 1)...prompt_at) || +"".b
96
109
  end
97
110
 
111
+ def stream_output(output, response, echo, state)
112
+ return unless output
113
+
114
+ echo_at = response.rindex(echo)
115
+ return unless echo_at
116
+
117
+ output_at = response.index("\n", echo_at + echo.bytesize)
118
+ return unless output_at
119
+
120
+ output_at += 1
121
+ prompt_at = response.index(PROMPT_START, output_at)
122
+ safe_end = prompt_at || (response.rindex("\n", response.bytesize - 1).to_i + 1)
123
+ position = state.fetch(:position, output_at)
124
+ return if safe_end <= position
125
+
126
+ output.write(remove_ruby_exception_status(response.byteslice(position...safe_end)))
127
+ output.flush if output.respond_to?(:flush)
128
+ state[:position] = safe_end
129
+ end
130
+
131
+ def remove_ruby_exception_status(data)
132
+ data.gsub(RUBY_EXCEPTION_STATUS, "")
133
+ end
134
+
98
135
  def drain_input
99
136
  deadline = monotonic_time + 0.1
100
137
  buffer = +"".b
@@ -114,9 +151,7 @@ module Rpremote
114
151
  def read_available(deadline)
115
152
  remaining = deadline - monotonic_time
116
153
  raise TimeoutError, "timed out waiting for the R2P2 Shell after #{timeout} seconds" unless remaining.positive?
117
- unless selectable_io.wait_readable(remaining)
118
- raise TimeoutError, "timed out waiting for the R2P2 Shell after #{timeout} seconds"
119
- end
154
+ raise TimeoutError, "timed out waiting for the R2P2 Shell after #{timeout} seconds" unless selectable_io.wait_readable(remaining)
120
155
 
121
156
  data = io.read_nonblock(4096)
122
157
  raise IOError, "serial connection closed" if data.nil? || data.empty?
@@ -155,7 +190,7 @@ module Rpremote
155
190
  end
156
191
 
157
192
  def monotonic_time
158
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
193
+ @clock.call
159
194
  end
160
195
  end
161
196
  end
@@ -49,7 +49,7 @@ module Rpremote
49
49
  data = serial_io.read_nonblock(BUFFER_SIZE)
50
50
  return false if data.nil? || data.empty?
51
51
 
52
- output.write(data)
52
+ output.write(normalize_line_endings(data))
53
53
  output.flush if output.respond_to?(:flush)
54
54
  true
55
55
  else
@@ -75,6 +75,18 @@ module Rpremote
75
75
  true
76
76
  end
77
77
 
78
+ # input.raw disables the terminal's usual LF-to-CRLF output conversion.
79
+ # Preserve device CRLF while making a bare LF start at the left margin.
80
+ def normalize_line_endings(data)
81
+ normalized = +"".b
82
+ data.each_byte do |byte|
83
+ normalized << "\r" if byte == 0x0A && !@previous_output_was_cr
84
+ normalized << byte
85
+ @previous_output_was_cr = byte == 0x0D
86
+ end
87
+ normalized
88
+ end
89
+
78
90
  def selectable_serial
79
91
  @selectable_serial ||= serial_io.respond_to?(:to_io) ? serial_io.to_io : serial_io
80
92
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rpremote
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/rpremote.rb CHANGED
@@ -19,7 +19,10 @@ require_relative "rpremote/terminal"
19
19
  require_relative "rpremote/shell"
20
20
  require_relative "rpremote/runner"
21
21
  require_relative "rpremote/resetter"
22
+ require_relative "rpremote/recursive_copy"
23
+ require_relative "rpremote/picoruby_source_patch"
22
24
  require_relative "rpremote/language_source"
25
+ require_relative "rpremote/nuke_firmware"
23
26
  require_relative "rpremote/mrbgems"
24
27
  require_relative "rpremote/mrbgems_command"
25
28
  require_relative "rpremote/flasher"
@@ -28,3 +31,5 @@ require_relative "rpremote/dfu_command"
28
31
  require_relative "rpremote/setup_command"
29
32
  require_relative "rpremote/build_command"
30
33
  require_relative "rpremote/flash_command"
34
+ require_relative "rpremote/bootsel_command"
35
+ require_relative "rpremote/deploy_command"
@@ -0,0 +1,71 @@
1
+ diff --git a/mrbgems/picoruby-machine/include/machine.h b/mrbgems/picoruby-machine/include/machine.h
2
+ --- a/mrbgems/picoruby-machine/include/machine.h
3
+ +++ b/mrbgems/picoruby-machine/include/machine.h
4
+ @@ -48,1 +48,2 @@
5
+ void Machine_reboot(void);
6
+ +void Machine_bootsel(void);
7
+ diff --git a/mrbgems/picoruby-machine/mrblib/machine.rb b/mrbgems/picoruby-machine/mrblib/machine.rb
8
+ --- a/mrbgems/picoruby-machine/mrblib/machine.rb
9
+ +++ b/mrbgems/picoruby-machine/mrblib/machine.rb
10
+ @@ -22,0 +23,4 @@
11
+ + def self.bootsel
12
+ + self._bootsel
13
+ + end
14
+ +
15
+ diff --git a/mrbgems/picoruby-machine/ports/rp2040/machine.c b/mrbgems/picoruby-machine/ports/rp2040/machine.c
16
+ --- a/mrbgems/picoruby-machine/ports/rp2040/machine.c
17
+ +++ b/mrbgems/picoruby-machine/ports/rp2040/machine.c
18
+ @@ -12,0 +13,1 @@
19
+ +#include "pico/bootrom.h"
20
+ @@ -32,0 +34,6 @@
21
+ +void
22
+ +Machine_bootsel(void)
23
+ +{
24
+ + rom_reset_usb_boot(0, 0);
25
+ +}
26
+ +
27
+ diff --git a/mrbgems/picoruby-machine/src/mruby/machine.c b/mrbgems/picoruby-machine/src/mruby/machine.c
28
+ --- a/mrbgems/picoruby-machine/src/mruby/machine.c
29
+ +++ b/mrbgems/picoruby-machine/src/mruby/machine.c
30
+ @@ -417,0 +418,11 @@
31
+ +static mrb_value
32
+ +mrb_s__bootsel(mrb_state *mrb, mrb_value self)
33
+ +{
34
+ +#if !defined(PICORB_PLATFORM_POSIX)
35
+ + Machine_bootsel();
36
+ +#else
37
+ + mrb_raise(mrb, E_NOTIMP_ERROR, "Machine._bootsel is not available on this platform");
38
+ +#endif
39
+ + return mrb_nil_value();
40
+ +}
41
+ +
42
+ @@ -443,0 +455,1 @@
43
+ + mrb_define_class_method_id(mrb, module_Machine, MRB_SYM(_bootsel), mrb_s__bootsel, MRB_ARGS_NONE());
44
+ diff --git a/mrbgems/picoruby-machine/src/mrubyc/machine.c b/mrbgems/picoruby-machine/src/mrubyc/machine.c
45
+ --- a/mrbgems/picoruby-machine/src/mrubyc/machine.c
46
+ +++ b/mrbgems/picoruby-machine/src/mrubyc/machine.c
47
+ @@ -252,0 +253,11 @@
48
+ +static void
49
+ +c_Machine__bootsel(mrbc_vm *vm, mrbc_value *v, int argc)
50
+ +{
51
+ +#if !defined(PICORB_PLATFORM_POSIX)
52
+ + Machine_bootsel();
53
+ +#else
54
+ + mrbc_raise(vm, MRBC_CLASS(RuntimeError), "Machine.bootsel is not available on this platform.");
55
+ +#endif
56
+ + SET_NIL_RETURN();
57
+ +}
58
+ +
59
+ @@ -439,0 +451,1 @@
60
+ + mrbc_define_method(vm, module_Machine, "_bootsel", c_Machine__bootsel);
61
+ diff --git a/mrbgems/picoruby-shell/shell_executables/_path.txt b/mrbgems/picoruby-shell/shell_executables/_path.txt
62
+ --- a/mrbgems/picoruby-shell/shell_executables/_path.txt
63
+ +++ b/mrbgems/picoruby-shell/shell_executables/_path.txt
64
+ @@ -16,0 +17,1 @@
65
+ +/bin/bootsel
66
+ diff --git a/mrbgems/picoruby-shell/shell_executables/bootsel.rb b/mrbgems/picoruby-shell/shell_executables/bootsel.rb
67
+ new file mode 100644
68
+ --- /dev/null
69
+ +++ b/mrbgems/picoruby-shell/shell_executables/bootsel.rb
70
+ @@ -0,0 +1 @@
71
+ +Machine.bootsel
@@ -0,0 +1,59 @@
1
+ diff --git a/mrbgems/picoruby-shell/mrblib/job.rb b/mrbgems/picoruby-shell/mrblib/job.rb
2
+ index 9351ee0..91e7e74 100644
3
+ --- a/mrbgems/picoruby-shell/mrblib/job.rb
4
+ +++ b/mrbgems/picoruby-shell/mrblib/job.rb
5
+ @@ -1,5 +1,7 @@
6
+ class Shell
7
+ class Job
8
+ + RUBY_EXCEPTION_STATUS = "\x1eR2P2:RUBY_EXCEPTION\x1f"
9
+ +
10
+ def initialize(*params)
11
+ if params.empty?
12
+ raise ArgumentError, "Job requires at least one parameter"
13
+ @@ -24,12 +26,11 @@ class Shell
14
+ @sandbox.resume
15
+ @sandbox.wait(timeout: nil)
16
+ if error = @sandbox.error
17
+ - puts "#{error.message} (#{error.class})"
18
+ + return report_ruby_exception(error)
19
+ end
20
+ return true
21
+ rescue Exception => e
22
+ - puts "#{e.message} (#{e.class})"
23
+ - return false
24
+ + return report_ruby_exception(e)
25
+ end
26
+
27
+ def state
28
+ @@ -46,22 +47,28 @@ class Shell
29
+ trap
30
+ @sandbox.load_file(@exefile)
31
+ if error = @sandbox.error
32
+ - puts "\n#{error.message} (#{error.class})"
33
+ + return report_ruby_exception(error, true)
34
+ end
35
+ return true
36
+ rescue Exception => e
37
+ - puts "#{e.message} (#{e.class})"
38
+ # backtrace uses a lot of memory
39
+ #if e.respond_to?(:backtrace)
40
+ # e.backtrace.each do |line|
41
+ # puts " #{line}"
42
+ # end
43
+ #end
44
+ - return false
45
+ + return report_ruby_exception(e)
46
+ end
47
+
48
+ private
49
+
50
+ + def report_ruby_exception(error, leading_newline = false)
51
+ + print "\n" if leading_newline
52
+ + puts "#{error.message} (#{error.class})"
53
+ + print RUBY_EXCEPTION_STATUS
54
+ + false
55
+ + end
56
+ +
57
+ def trap
58
+ Signal.trap(:TSTP) do
59
+ @sandbox.suspend