srsh 0.7.1 → 1.0.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/LICENSE +18 -14
- data/README.md +351 -134
- data/bin/srsh +53 -1473
- data/docs/assets/slut.txt +4 -0
- data/docs/assets/srsh-mark.svg +12 -0
- data/docs/css/style.css +696 -0
- data/docs/index.html +703 -0
- data/docs/js/app.js +203 -0
- data/examples/bridge.rsh +8 -0
- data/examples/calculator.rsh +253 -0
- data/examples/defer.rsh +14 -0
- data/examples/hot.rsh +14 -0
- data/examples/meta.rsh +20 -0
- data/examples/modules/text.rsh +6 -0
- data/examples/modules.rsh +6 -0
- data/examples/paste.rsh +15 -0
- data/examples/plugin.rb +8 -0
- data/examples/power.rsh +65 -0
- data/examples/tour.rsh +38 -0
- data/ext/srsh_native/extconf.rb +3 -0
- data/ext/srsh_native/srsh_native.c +48 -0
- data/language-docs/LANGUAGE.md +670 -0
- data/language-docs/MIGRATION.md +44 -0
- data/language-docs/SECURITY.md +44 -0
- data/lib/srsh/app.rb +261 -0
- data/lib/srsh/builtins.rb +492 -0
- data/lib/srsh/editor.rb +530 -0
- data/lib/srsh/errors.rb +23 -0
- data/lib/srsh/history.rb +74 -0
- data/lib/srsh/language/evaluator.rb +1175 -0
- data/lib/srsh/language/lexer.rb +316 -0
- data/lib/srsh/language/parser.rb +997 -0
- data/lib/srsh/language/token.rb +5 -0
- data/lib/srsh/language/values.rb +392 -0
- data/lib/srsh/paths.rb +29 -0
- data/lib/srsh/plugins.rb +59 -0
- data/lib/srsh/process_identity.rb +38 -0
- data/lib/srsh/security.rb +38 -0
- data/lib/srsh/shell/executor.rb +1182 -0
- data/lib/srsh/shell/job.rb +101 -0
- data/lib/srsh/shell/lexer.rb +114 -0
- data/lib/srsh/shell/terminal.rb +26 -0
- data/lib/srsh/state.rb +136 -0
- data/lib/srsh/theme.rb +108 -0
- data/lib/srsh/version.rb +1 -2
- data/lib/srsh.rb +15 -0
- metadata +59 -11
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'fiddle'
|
|
3
|
+
require_relative '../errors'
|
|
4
|
+
|
|
5
|
+
module Srsh
|
|
6
|
+
module Language
|
|
7
|
+
class FunctionRef
|
|
8
|
+
attr_reader :name, :captured
|
|
9
|
+
def initialize(name, captured = nil)
|
|
10
|
+
@name = name.to_s.freeze
|
|
11
|
+
@captured = captured
|
|
12
|
+
end
|
|
13
|
+
def to_s = "fn<#{name}>"
|
|
14
|
+
def inspect = to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
PrototypeRef = Data.define(:name) do
|
|
18
|
+
def to_s = name.to_s
|
|
19
|
+
def inspect = to_s
|
|
20
|
+
end
|
|
21
|
+
BoundMethodValue = Data.define(:receiver, :name)
|
|
22
|
+
NativeMethodValue = Data.define(:receiver, :name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NativePointerValue
|
|
27
|
+
attr_reader :pointer
|
|
28
|
+
def initialize(pointer)
|
|
29
|
+
@pointer = pointer.is_a?(Fiddle::Pointer) ? pointer : Fiddle::Pointer.new(pointer.to_i)
|
|
30
|
+
end
|
|
31
|
+
def address = @pointer.to_i
|
|
32
|
+
def null? = address.zero?
|
|
33
|
+
def to_s = "ptr<0x#{address.to_s(16)}>"
|
|
34
|
+
def inspect = to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class CBufferValue
|
|
38
|
+
attr_reader :pointer, :size
|
|
39
|
+
def initialize(size)
|
|
40
|
+
@size = Integer(size)
|
|
41
|
+
raise RuntimeError, 'cbuf() size must be between 1 and 64 MiB' unless @size.between?(1, 64 * 1024 * 1024)
|
|
42
|
+
@pointer = Fiddle::Pointer.malloc(@size, Fiddle::RUBY_FREE)
|
|
43
|
+
@pointer[0, @size] = "\0" * @size
|
|
44
|
+
end
|
|
45
|
+
def address = @pointer.to_i
|
|
46
|
+
def to_s = "cbuf<#{@size}@0x#{address.to_s(16)}>"
|
|
47
|
+
def inspect = to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class NativeFunctionValue
|
|
51
|
+
TYPES = {
|
|
52
|
+
'void' => Fiddle::TYPE_VOID,
|
|
53
|
+
# C _Bool has the size and alignment of an unsigned char. Ruby 3.2's
|
|
54
|
+
# Fiddle does not expose TYPE_BOOL, so use its portable ABI spelling.
|
|
55
|
+
'bool' => Fiddle::TYPE_UINT8_T,
|
|
56
|
+
'i8' => Fiddle::TYPE_INT8_T, 'u8' => Fiddle::TYPE_UINT8_T,
|
|
57
|
+
'i16' => Fiddle::TYPE_INT16_T, 'u16' => Fiddle::TYPE_UINT16_T,
|
|
58
|
+
'i32' => Fiddle::TYPE_INT32_T, 'u32' => Fiddle::TYPE_UINT32_T,
|
|
59
|
+
'i64' => Fiddle::TYPE_INT64_T, 'u64' => Fiddle::TYPE_UINT64_T,
|
|
60
|
+
'isize' => Fiddle::TYPE_INTPTR_T, 'usize' => Fiddle::TYPE_SIZE_T,
|
|
61
|
+
'f32' => Fiddle::TYPE_FLOAT, 'f64' => Fiddle::TYPE_DOUBLE,
|
|
62
|
+
'cstr' => Fiddle::TYPE_CONST_STRING, 'ptr' => Fiddle::TYPE_VOIDP
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
attr_reader :name, :params, :result
|
|
66
|
+
|
|
67
|
+
def initialize(handle, name, params, result)
|
|
68
|
+
@name = name.to_s.freeze
|
|
69
|
+
@params = params.map(&:to_s).freeze
|
|
70
|
+
@result = result.to_s.freeze
|
|
71
|
+
bad = (@params + [@result]).reject { |type| TYPES.key?(type) }
|
|
72
|
+
raise RuntimeError, "unknown C ABI type #{bad.first.inspect}" unless bad.empty?
|
|
73
|
+
raise RuntimeError, 'void is only valid as a return type' if @params.include?('void')
|
|
74
|
+
address = handle[@name]
|
|
75
|
+
@function = Fiddle::Function.new(address, @params.map { |t| TYPES.fetch(t) }, TYPES.fetch(@result))
|
|
76
|
+
rescue Fiddle::DLError => e
|
|
77
|
+
raise RuntimeError, "C symbol #{@name.inspect}: #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def call(*args)
|
|
81
|
+
raise RuntimeError, "#{@name} expects #{@params.length} arguments, got #{args.length}" unless args.length == @params.length
|
|
82
|
+
marshaled = args.zip(@params).map { |value, type| marshal_arg(value, type) }
|
|
83
|
+
convert_result(@function.call(*marshaled))
|
|
84
|
+
rescue Fiddle::DLError, TypeError, ArgumentError => e
|
|
85
|
+
raise RuntimeError, "C call #{@name}: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_s = "cfn<#{@name}(#{@params.join(',')}) -> #{@result}>"
|
|
89
|
+
def inspect = to_s
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def marshal_arg(value, type)
|
|
94
|
+
case type
|
|
95
|
+
when 'cstr' then value.to_s
|
|
96
|
+
when 'ptr'
|
|
97
|
+
case value
|
|
98
|
+
when nil then 0
|
|
99
|
+
when NativePointerValue then value.pointer
|
|
100
|
+
when CBufferValue then value.pointer
|
|
101
|
+
when Fiddle::Pointer then value
|
|
102
|
+
when Integer then value
|
|
103
|
+
else raise RuntimeError, "#{@name}: ptr argument requires cbuf/ptr/int/void"
|
|
104
|
+
end
|
|
105
|
+
when 'f32', 'f64' then Float(value)
|
|
106
|
+
when 'bool' then value ? 1 : 0
|
|
107
|
+
else Integer(value)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def convert_result(value)
|
|
112
|
+
case @result
|
|
113
|
+
when 'void' then nil
|
|
114
|
+
when 'bool' then value != 0 && value != false
|
|
115
|
+
when 'cstr'
|
|
116
|
+
return nil if value.nil? || (value.respond_to?(:to_i) && value.to_i.zero?)
|
|
117
|
+
value.is_a?(Fiddle::Pointer) ? value.to_s : Fiddle::Pointer.new(value.to_i).to_s
|
|
118
|
+
when 'ptr'
|
|
119
|
+
NativePointerValue.new(value || 0)
|
|
120
|
+
else value
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class CommandValue
|
|
126
|
+
attr_reader :argv
|
|
127
|
+
def initialize(argv)
|
|
128
|
+
@argv = argv.map(&:to_s).freeze
|
|
129
|
+
raise RuntimeError, 'cmd() requires at least one argv item' if @argv.empty? || @argv[0].empty?
|
|
130
|
+
end
|
|
131
|
+
def to_s = "cmd<#{@argv.map(&:inspect).join(' ')}>"
|
|
132
|
+
def inspect = to_s
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class NamespaceValue
|
|
136
|
+
attr_reader :name, :members
|
|
137
|
+
|
|
138
|
+
def initialize(name)
|
|
139
|
+
@name = name.to_s.freeze
|
|
140
|
+
@members = {}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def member?(name) = @members.key?(name.to_s)
|
|
144
|
+
def get(name) = @members[name.to_s]
|
|
145
|
+
def set(name, value) = (@members[name.to_s] = value)
|
|
146
|
+
def keys = @members.keys.sort
|
|
147
|
+
def to_s = "space<#{@name}>"
|
|
148
|
+
def inspect = to_s
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class NativeLibraryValue < NamespaceValue
|
|
152
|
+
attr_reader :path, :handle
|
|
153
|
+
def initialize(name, path)
|
|
154
|
+
super(name)
|
|
155
|
+
@path = path.to_s.freeze
|
|
156
|
+
@handle = %w[@self self process].include?(@path) ? Fiddle::Handle::DEFAULT : Fiddle::Handle.new(@path)
|
|
157
|
+
rescue Fiddle::DLError => e
|
|
158
|
+
raise RuntimeError, "cannot load C library #{@path.inspect}: #{e.message}"
|
|
159
|
+
end
|
|
160
|
+
def to_s = "bridge<#{name}:#{@path}>"
|
|
161
|
+
def inspect = to_s
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ObjectValue
|
|
166
|
+
attr_reader :proto_name
|
|
167
|
+
|
|
168
|
+
def initialize(proto_name, fields = {})
|
|
169
|
+
@proto_name = proto_name.to_s.freeze
|
|
170
|
+
@fields = fields.dup
|
|
171
|
+
@lock = Mutex.new
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def field?(name)
|
|
175
|
+
key = name.to_s
|
|
176
|
+
@lock.synchronize { @fields.key?(key) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def get(name)
|
|
180
|
+
key = name.to_s
|
|
181
|
+
@lock.synchronize { @fields[key] }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def set(name, value)
|
|
185
|
+
key = name.to_s
|
|
186
|
+
@lock.synchronize { @fields[key] = value }
|
|
187
|
+
value
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def update(name)
|
|
191
|
+
key = name.to_s
|
|
192
|
+
@lock.synchronize do
|
|
193
|
+
@fields[key] = yield(@fields[key])
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def fields
|
|
198
|
+
@lock.synchronize { @fields.dup }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def copy
|
|
202
|
+
self.class.new(@proto_name, fields)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def to_s
|
|
206
|
+
body = fields.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
|
|
207
|
+
"#{@proto_name}{#{body}}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def inspect = to_s
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
class TaskValue
|
|
214
|
+
def initialize(&work)
|
|
215
|
+
@mutex = Mutex.new
|
|
216
|
+
@cv = ConditionVariable.new
|
|
217
|
+
@state = :pending
|
|
218
|
+
@value = nil
|
|
219
|
+
@error = nil
|
|
220
|
+
@thread = Thread.new do
|
|
221
|
+
begin
|
|
222
|
+
cancelled = @mutex.synchronize do
|
|
223
|
+
if @state == :cancelled
|
|
224
|
+
true
|
|
225
|
+
else
|
|
226
|
+
@state = :running
|
|
227
|
+
false
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
next if cancelled
|
|
231
|
+
|
|
232
|
+
value = work.call
|
|
233
|
+
@mutex.synchronize do
|
|
234
|
+
unless @state == :cancelled
|
|
235
|
+
@value = value
|
|
236
|
+
@state = :done
|
|
237
|
+
end
|
|
238
|
+
@cv.broadcast
|
|
239
|
+
end
|
|
240
|
+
rescue Exception => e # task boundary: preserve error for await()
|
|
241
|
+
@mutex.synchronize do
|
|
242
|
+
unless @state == :cancelled
|
|
243
|
+
@error = e
|
|
244
|
+
@state = :failed
|
|
245
|
+
end
|
|
246
|
+
@cv.broadcast
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def await(timeout = nil)
|
|
253
|
+
timeout = timeout&.to_f
|
|
254
|
+
deadline = timeout ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil
|
|
255
|
+
@mutex.synchronize do
|
|
256
|
+
until %i[done failed cancelled].include?(@state)
|
|
257
|
+
if deadline
|
|
258
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
259
|
+
raise RuntimeError, 'task await timed out' if remaining <= 0
|
|
260
|
+
@cv.wait(@mutex, remaining)
|
|
261
|
+
else
|
|
262
|
+
@cv.wait(@mutex)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
raise RuntimeError, "task cancelled" if @state == :cancelled
|
|
266
|
+
if @error
|
|
267
|
+
raise @error if @error.is_a?(Srsh::Error)
|
|
268
|
+
raise RuntimeError, "task failed: #{@error.class}: #{@error.message}"
|
|
269
|
+
end
|
|
270
|
+
@value
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def done?
|
|
275
|
+
@mutex.synchronize { %i[done failed cancelled].include?(@state) }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def status
|
|
279
|
+
@mutex.synchronize { @state.to_s }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def cancel
|
|
283
|
+
thread = nil
|
|
284
|
+
@mutex.synchronize do
|
|
285
|
+
return false if %i[done failed cancelled].include?(@state)
|
|
286
|
+
@state = :cancelled
|
|
287
|
+
thread = @thread
|
|
288
|
+
@cv.broadcast
|
|
289
|
+
end
|
|
290
|
+
thread.kill if thread&.alive?
|
|
291
|
+
true
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def thread = @thread
|
|
295
|
+
def to_s = "task<#{status}>"
|
|
296
|
+
def inspect = to_s
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
class ChannelValue
|
|
300
|
+
CLOSED = Object.new.freeze
|
|
301
|
+
|
|
302
|
+
def initialize(capacity = 0)
|
|
303
|
+
capacity = capacity.to_i
|
|
304
|
+
raise RuntimeError, 'channel capacity cannot be negative' if capacity.negative?
|
|
305
|
+
@capacity = capacity
|
|
306
|
+
@queue = []
|
|
307
|
+
@closed = false
|
|
308
|
+
@mutex = Mutex.new
|
|
309
|
+
@readable = ConditionVariable.new
|
|
310
|
+
@writable = ConditionVariable.new
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def send_value(value)
|
|
314
|
+
@mutex.synchronize do
|
|
315
|
+
raise RuntimeError, 'send on closed channel' if @closed
|
|
316
|
+
while @capacity.positive? && @queue.length >= @capacity
|
|
317
|
+
@writable.wait(@mutex)
|
|
318
|
+
raise RuntimeError, 'send on closed channel' if @closed
|
|
319
|
+
end
|
|
320
|
+
@queue << value
|
|
321
|
+
@readable.signal
|
|
322
|
+
end
|
|
323
|
+
value
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def recv(timeout = nil)
|
|
327
|
+
timeout = timeout&.to_f
|
|
328
|
+
deadline = timeout ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil
|
|
329
|
+
@mutex.synchronize do
|
|
330
|
+
while @queue.empty? && !@closed
|
|
331
|
+
if deadline
|
|
332
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
333
|
+
raise RuntimeError, 'channel receive timed out' if remaining <= 0
|
|
334
|
+
@readable.wait(@mutex, remaining)
|
|
335
|
+
else
|
|
336
|
+
@readable.wait(@mutex)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
return nil if @queue.empty? && @closed
|
|
340
|
+
value = @queue.shift
|
|
341
|
+
@writable.signal
|
|
342
|
+
value
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def try_recv
|
|
347
|
+
@mutex.synchronize do
|
|
348
|
+
return nil if @queue.empty?
|
|
349
|
+
value = @queue.shift
|
|
350
|
+
@writable.signal
|
|
351
|
+
value
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def close
|
|
356
|
+
@mutex.synchronize do
|
|
357
|
+
return false if @closed
|
|
358
|
+
@closed = true
|
|
359
|
+
@readable.broadcast
|
|
360
|
+
@writable.broadcast
|
|
361
|
+
end
|
|
362
|
+
true
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def closed? = @mutex.synchronize { @closed }
|
|
366
|
+
def size = @mutex.synchronize { @queue.length }
|
|
367
|
+
def to_s = "chan<#{size}#{closed? ? ',closed' : ''}>"
|
|
368
|
+
def inspect = to_s
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
class AtomValue
|
|
372
|
+
def initialize(value)
|
|
373
|
+
@value = value
|
|
374
|
+
@lock = Mutex.new
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def get = @lock.synchronize { @value }
|
|
378
|
+
|
|
379
|
+
def set(value)
|
|
380
|
+
@lock.synchronize { @value = value }
|
|
381
|
+
value
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def swap
|
|
385
|
+
@lock.synchronize { @value = yield(@value) }
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def to_s = "atom<#{get.inspect}>"
|
|
389
|
+
def inspect = to_s
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
data/lib/srsh/paths.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
|
|
3
|
+
module Srsh
|
|
4
|
+
class Paths
|
|
5
|
+
attr_reader :home, :root, :plugins, :themes, :config, :history, :history_v1, :rc, :theme_state
|
|
6
|
+
|
|
7
|
+
def initialize(home = Dir.home)
|
|
8
|
+
@home = home
|
|
9
|
+
@root = File.join(home, '.srsh')
|
|
10
|
+
@plugins = File.join(root, 'plugins')
|
|
11
|
+
@themes = File.join(root, 'themes')
|
|
12
|
+
@config = File.join(root, 'config')
|
|
13
|
+
# Keep the original SRSH history location. 1.0.0 briefly moved it into
|
|
14
|
+
# ~/.srsh/history; History imports that file so neither generation is lost.
|
|
15
|
+
@history = File.join(home, '.srsh_history')
|
|
16
|
+
@history_v1 = File.join(root, 'history')
|
|
17
|
+
@rc = File.join(home, '.srshrc')
|
|
18
|
+
@theme_state = File.join(root, 'theme')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ensure!
|
|
22
|
+
FileUtils.mkdir_p(root, mode: 0o700)
|
|
23
|
+
FileUtils.mkdir_p(plugins, mode: 0o700)
|
|
24
|
+
FileUtils.mkdir_p(themes, mode: 0o700)
|
|
25
|
+
[root, plugins, themes].each { |p| File.chmod(0o700, p) rescue nil }
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/srsh/plugins.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative 'security'
|
|
2
|
+
|
|
3
|
+
module Srsh
|
|
4
|
+
class PluginAPI
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def builtin(name, &block) = @app.builtins.register(name, &block)
|
|
10
|
+
def hook(type, &block) = @app.state.hook(type, &block)
|
|
11
|
+
def alias_command(name, command) = @app.state.aliases[name.to_s] = command.to_s
|
|
12
|
+
def theme(name, values) = @app.theme.register(name, values)
|
|
13
|
+
def state = @app.state
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Plugins
|
|
17
|
+
MAX_PLUGIN_BYTES = 4 * 1024 * 1024
|
|
18
|
+
attr_reader :loaded
|
|
19
|
+
|
|
20
|
+
def initialize(app)
|
|
21
|
+
@app = app
|
|
22
|
+
@loaded = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def load_all
|
|
26
|
+
@loaded.clear
|
|
27
|
+
Dir.glob(File.join(@app.paths.plugins, '*.{rsh,rb}')).sort.each { |path| load_file(path) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def load_file(path)
|
|
31
|
+
unless Security.private_regular_file?(path)
|
|
32
|
+
@app.err.puts @app.theme.paint("plugin refused (unsafe owner/mode): #{path}", :warn, io: @app.err)
|
|
33
|
+
return false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
case File.extname(path)
|
|
37
|
+
when '.rsh'
|
|
38
|
+
@app.run_script(path, [])
|
|
39
|
+
when '.rb'
|
|
40
|
+
api = PluginAPI.new(@app)
|
|
41
|
+
mod = Module.new
|
|
42
|
+
mod.const_set(:SRSH, api)
|
|
43
|
+
raise Error, 'Ruby plugin too large' if File.size(path) > MAX_PLUGIN_BYTES
|
|
44
|
+
source = File.binread(path, MAX_PLUGIN_BYTES + 1)
|
|
45
|
+
raise Error, 'Ruby plugin too large' if source.bytesize > MAX_PLUGIN_BYTES
|
|
46
|
+
source.force_encoding(Encoding::UTF_8)
|
|
47
|
+
raise Error, 'Ruby plugin is not valid UTF-8' unless source.valid_encoding?
|
|
48
|
+
mod.module_eval(source, path, 1)
|
|
49
|
+
else
|
|
50
|
+
return false
|
|
51
|
+
end
|
|
52
|
+
@loaded << path
|
|
53
|
+
true
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
@app.err.puts @app.theme.paint("plugin #{File.basename(path)}: #{e.class}: #{e.message}", :error, io: @app.err)
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Srsh
|
|
4
|
+
module ProcessIdentity
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def install!(executable: $PROGRAM_NAME)
|
|
8
|
+
path = File.expand_path(executable.to_s)
|
|
9
|
+
ENV['SHELL'] = path unless path.empty?
|
|
10
|
+
$0 = 'srsh'
|
|
11
|
+
if RUBY_PLATFORM.include?('linux')
|
|
12
|
+
if defined?(::SrshNative) && ::SrshNative.respond_to?(:set_process_name)
|
|
13
|
+
::SrshNative.set_process_name('srsh')
|
|
14
|
+
else
|
|
15
|
+
linux_process_name('srsh')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
true
|
|
19
|
+
rescue StandardError
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def linux_process_name(name)
|
|
24
|
+
require 'fiddle'
|
|
25
|
+
libc = Fiddle::Handle::DEFAULT
|
|
26
|
+
prctl = Fiddle::Function.new(
|
|
27
|
+
libc['prctl'],
|
|
28
|
+
[Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_ULONG, Fiddle::TYPE_ULONG, Fiddle::TYPE_ULONG],
|
|
29
|
+
Fiddle::TYPE_INT
|
|
30
|
+
)
|
|
31
|
+
bytes = name.to_s.byteslice(0, 15).to_s + "\0"
|
|
32
|
+
pointer = Fiddle::Pointer[bytes]
|
|
33
|
+
prctl.call(15, pointer, 0, 0, 0) # PR_SET_NAME
|
|
34
|
+
rescue Fiddle::DLError, LoadError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
|
|
3
|
+
module Srsh
|
|
4
|
+
module Security
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def private_regular_file?(path)
|
|
8
|
+
st = File.lstat(path)
|
|
9
|
+
return false unless st.file?
|
|
10
|
+
return false if st.symlink?
|
|
11
|
+
return false unless st.uid == Process.uid
|
|
12
|
+
(st.mode & 0o022).zero?
|
|
13
|
+
rescue SystemCallError
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def safe_auto_source?(path)
|
|
18
|
+
!File.exist?(path) || private_regular_file?(path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def atomic_write(path, data, mode: 0o600)
|
|
22
|
+
dir = File.dirname(path)
|
|
23
|
+
Dir.mkdir(dir, 0o700) unless Dir.exist?(dir)
|
|
24
|
+
tmp = "#{path}.tmp.#{$$}.#{rand(1 << 30)}"
|
|
25
|
+
flags = File::WRONLY | File::CREAT | File::EXCL
|
|
26
|
+
File.open(tmp, flags, mode) do |f|
|
|
27
|
+
f.write(data)
|
|
28
|
+
f.flush
|
|
29
|
+
f.fsync rescue nil
|
|
30
|
+
end
|
|
31
|
+
File.rename(tmp, path)
|
|
32
|
+
File.chmod(mode, path) rescue nil
|
|
33
|
+
true
|
|
34
|
+
ensure
|
|
35
|
+
File.unlink(tmp) if defined?(tmp) && tmp && File.exist?(tmp)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|