Class: Aspera::Environment
- Inherits:
-
Object
- Object
- Aspera::Environment
- Includes:
- Singleton
- Defined in:
- lib/aspera/environment.rb
Overview
detect OS, architecture, and specific stuff
Constant Summary collapse
- OS_WINDOWS =
:windows- OS_MACOS =
:osx- OS_LINUX =
:linux- OS_AIX =
:aix- OS_LIST =
[OS_WINDOWS, OS_MACOS, OS_LINUX, OS_AIX].freeze
- CPU_X86_64 =
:x86_64- CPU_ARM64 =
:arm64- CPU_PPC64 =
:ppc64- CPU_PPC64LE =
:ppc64le- CPU_S390 =
:s390- CPU_LIST =
[CPU_X86_64, CPU_ARM64, CPU_PPC64, CPU_PPC64LE, CPU_S390].freeze
- BITS_PER_BYTE =
8- MEBI =
1024 * 1024
- BYTES_PER_MEBIBIT =
MEBI / BITS_PER_BYTE
- I18N_VARS =
%w(LC_ALL LC_CTYPE LANG).freeze
- WINDOWS_FILENAME_INVALID_CHARACTERS =
“/” is invalid on both Unix and Windows, other are Windows special characters See: learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
'<>:"/\\|?*'- REPLACE_CHARACTER =
'_'- RB_EXT =
'.rb'
Instance Attribute Summary collapse
-
#cpu ⇒ Object
readonly
Returns the value of attribute cpu.
-
#default_gui_mode ⇒ Object
readonly
Returns the value of attribute default_gui_mode.
-
#file_illegal_characters ⇒ Object
Returns the value of attribute file_illegal_characters.
-
#os ⇒ Object
readonly
Returns the value of attribute os.
-
#url_method ⇒ Object
Returns the value of attribute url_method.
Class Method Summary collapse
-
.empty_binding ⇒ Object
empty variable binding for secure eval.
-
.force_terminal_c ⇒ Object
force locale to C so that unicode characters are not used.
-
.log_spawn(exec:, args: nil, env: nil) ⇒ String
Generate log line for external program with arguments.
-
.restrict_file_access(path, mode: nil) ⇒ Object
restrict access to a file or folder to user only.
- .ruby_version ⇒ Object
-
.secure_capture(exec:, args: [], env: nil, exception: true, **kwargs) ⇒ Object
Execute process and capture stdout.
-
.secure_eval(code, file, line) ⇒ Object
secure execution of Ruby code.
-
.secure_execute(exec:, args: nil, env: nil, **kwargs) ⇒ Object
Start process (not in shell) and wait for completion.
-
.secure_spawn(exec:, args: nil, env: nil, **kwargs) ⇒ String
Start process in background caller can call Process.wait on returned value.
-
.terminal? ⇒ Boolean
True if we are in a terminal.
- .terminal_supports_unicode? ⇒ Boolean
-
.write_file_restricted(path, force: false, mode: nil) ⇒ Object
Write content to a file, with restricted access.
Instance Method Summary collapse
-
#architecture ⇒ Object
Normalized architecture name See constants: OS_* and CPU_*.
-
#exe_file(name = nil) ⇒ String
Add executable file extension (e.g. “.exe”) for current OS.
-
#fix_home ⇒ Object
on Windows, the env var %USERPROFILE% provides the path to user’s home more reliably than %HOMEDRIVE%%HOMEPATH% so, tell Ruby the right way.
- #graphical? ⇒ Boolean
-
#initialize ⇒ Environment
constructor
A new instance of Environment.
-
#initialize_fields ⇒ Object
initialize fields from environment.
-
#open_editor(file_path) ⇒ Object
open a file in an editor.
-
#open_uri(the_url) ⇒ Object
Allows a user to open a URL if method is :text, then URL is displayed on terminal if method is :graphical, then the URL will be opened with the default browser.
-
#open_uri_graphical(uri) ⇒ Object
Open a URI in a graphical browser Command must be non blocking.
-
#safe_filename_character ⇒ Object
Replacement character for illegal filename characters Can also be used as safe “join” character.
-
#sanitized_filename(filename) ⇒ String
Sanitize a filename by replacing illegal characters.
Constructor Details
#initialize ⇒ Environment
Returns a new instance of Environment.
200 201 202 |
# File 'lib/aspera/environment.rb', line 200 def initialize initialize_fields end |
Instance Attribute Details
#cpu ⇒ Object (readonly)
Returns the value of attribute cpu.
198 199 200 |
# File 'lib/aspera/environment.rb', line 198 def cpu @cpu end |
#default_gui_mode ⇒ Object (readonly)
Returns the value of attribute default_gui_mode.
198 199 200 |
# File 'lib/aspera/environment.rb', line 198 def default_gui_mode @default_gui_mode end |
#file_illegal_characters ⇒ Object
Returns the value of attribute file_illegal_characters.
197 198 199 |
# File 'lib/aspera/environment.rb', line 197 def file_illegal_characters @file_illegal_characters end |
#os ⇒ Object (readonly)
Returns the value of attribute os.
198 199 200 |
# File 'lib/aspera/environment.rb', line 198 def os @os end |
#url_method ⇒ Object
Returns the value of attribute url_method.
197 198 199 |
# File 'lib/aspera/environment.rb', line 197 def url_method @url_method end |
Class Method Details
.empty_binding ⇒ Object
empty variable binding for secure eval
51 52 53 |
# File 'lib/aspera/environment.rb', line 51 def empty_binding return Kernel.binding end |
.force_terminal_c ⇒ Object
force locale to C so that unicode characters are not used
186 187 188 |
# File 'lib/aspera/environment.rb', line 186 def force_terminal_c I18N_VARS.each{ |var| ENV[var] = 'C'} end |
.log_spawn(exec:, args: nil, env: nil) ⇒ String
Generate log line for external program with arguments
65 66 67 68 69 70 71 72 |
# File 'lib/aspera/environment.rb', line 65 def log_spawn(exec:, args: nil, env: nil) [ 'execute:'.red, env&.map{ |k, v| "#{k}=#{Shellwords.shellescape(v)}"}, Shellwords.shellescape(exec), args&.map{ |a| Shellwords.shellescape(a)} ].compact.flatten.join(' ') end |
.restrict_file_access(path, mode: nil) ⇒ Object
restrict access to a file or folder to user only
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/aspera/environment.rb', line 164 def restrict_file_access(path, mode: nil) if mode.nil? # or FileUtils ? if File.file?(path) mode = 0o600 elsif File.directory?(path) mode = 0o700 else Log.log.debug{"No restriction can be set for #{path}"} end end File.chmod(mode, path) unless mode.nil? rescue => e Log.log.warn(e.) end |
.ruby_version ⇒ Object
46 47 48 |
# File 'lib/aspera/environment.rb', line 46 def ruby_version return RbConfig::CONFIG['RUBY_PROGRAM_VERSION'] end |
.secure_capture(exec:, args: [], env: nil, exception: true, **kwargs) ⇒ Object
Execute process and capture stdout
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/aspera/environment.rb', line 129 def secure_capture(exec:, args: [], env: nil, exception: true, **kwargs) Aspera.assert_type(exec, String) Aspera.assert_type(args, Array) Log.log.debug{log_spawn(exec: exec, args: args, env: env)} Log.dump(:kwargs, kwargs, level: :trace2) # Log.dump(:ENV, ENV.to_h, level: :trace1) capture_args = [] capture_args.push(env) unless env.nil? capture_args.push(exec) capture_args.concat(args) stdout, stderr, status = Open3.capture3(*capture_args, **kwargs) Log.log.debug{"status=#{status}, stderr=#{stderr}"} Log.log.trace1{"stdout=#{stdout}"} raise "process failed: #{status.exitstatus} (#{stderr})" if !status.success? && exception return stdout end |
.secure_eval(code, file, line) ⇒ Object
secure execution of Ruby code
56 57 58 |
# File 'lib/aspera/environment.rb', line 56 def secure_eval(code, file, line) Kernel.send('lave'.reverse, code, empty_binding, file, line) end |
.secure_execute(exec:, args: nil, env: nil, **kwargs) ⇒ Object
Start process (not in shell) and wait for completion. By default, sets ‘exception: true` in `kwargs`
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/aspera/environment.rb', line 106 def secure_execute(exec:, args: nil, env: nil, **kwargs) Aspera.assert_type(exec, String) Aspera.assert_type(args, Array, NilClass) Aspera.assert_type(env, Hash, NilClass) Log.log.debug{log_spawn(exec: exec, args: args, env: env)} Log.dump(:kwargs, kwargs, level: :trace1) spawn_args = [] spawn_args.push(env) unless env.nil? # Ensure no shell expansion spawn_args.push([exec, exec]) spawn_args.concat(args) unless args.nil? # By default: exception on error kwargs[:exception] = true unless kwargs.key?(:exception) # Start in separate process Kernel.system(*spawn_args, **kwargs) nil end |
.secure_spawn(exec:, args: nil, env: nil, **kwargs) ⇒ String
Start process in background caller can call Process.wait on returned value
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/aspera/environment.rb', line 82 def secure_spawn(exec:, args: nil, env: nil, **kwargs) Aspera.assert_type(exec, String) Aspera.assert_type(args, Array, NilClass) Aspera.assert_type(env, Hash, NilClass) Log.log.debug{log_spawn(exec: exec, args: args, env: env)} spawn_args = [] spawn_args.push(env) unless env.nil? spawn_args.push([exec, exec]) spawn_args.concat(args) unless args.nil? kwargs[:close_others] = true unless kwargs.key?(:close_others) # Start separate process in background pid = Process.spawn(*spawn_args, **kwargs) Log.dump(:pid, pid) return pid end |
.terminal? ⇒ Boolean
Returns true if we are in a terminal.
181 182 183 |
# File 'lib/aspera/environment.rb', line 181 def terminal? $stdout.tty? end |
.terminal_supports_unicode? ⇒ Boolean
193 194 195 |
# File 'lib/aspera/environment.rb', line 193 def terminal_supports_unicode? terminal? && I18N_VARS.any?{ |var| ENV[var]&.include?('UTF-8')} end |
.write_file_restricted(path, force: false, mode: nil) ⇒ Object
Write content to a file, with restricted access
151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/aspera/environment.rb', line 151 def write_file_restricted(path, force: false, mode: nil) Aspera.assert(block_given?, type: Aspera::InternalError) if force || !File.exist?(path) # Windows may give error File.unlink(path) rescue nil # content provided by block File.write(path, yield) restrict_file_access(path, mode: mode) end return path end |
Instance Method Details
#architecture ⇒ Object
Normalized architecture name See constants: OS_* and CPU_*
247 248 249 |
# File 'lib/aspera/environment.rb', line 247 def architecture "#{@os}-#{@cpu}" end |
#exe_file(name = nil) ⇒ String
Add executable file extension (e.g. “.exe”) for current OS
254 255 256 257 |
# File 'lib/aspera/environment.rb', line 254 def exe_file(name = nil) return name unless @executable_extension return "#{name}#{@executable_extension}" end |
#fix_home ⇒ Object
on Windows, the env var %USERPROFILE% provides the path to user’s home more reliably than %HOMEDRIVE%%HOMEPATH% so, tell Ruby the right way
261 262 263 264 265 |
# File 'lib/aspera/environment.rb', line 261 def fix_home return unless @os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil)) ENV['HOME'] = ENV.fetch('USERPROFILE', nil) Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"} end |
#graphical? ⇒ Boolean
267 268 269 |
# File 'lib/aspera/environment.rb', line 267 def graphical? @default_gui_mode == :graphical end |
#initialize_fields ⇒ Object
initialize fields from environment
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/aspera/environment.rb', line 205 def initialize_fields @os = case RbConfig::CONFIG['host_os'] when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/ OS_WINDOWS when /darwin/, /mac os/ OS_MACOS when /linux/ OS_LINUX when /aix/ OS_AIX else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']){'host_os'} end @cpu = case RbConfig::CONFIG['host_cpu'] when /x86_64/, /x64/ CPU_X86_64 when /powerpc/, /ppc64/ @os.eql?(OS_LINUX) ? CPU_PPC64LE : CPU_PPC64 when /s390/ CPU_S390 when /arm/, /aarch64/ CPU_ARM64 else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']){'host_cpu'} end @executable_extension = @os.eql?(OS_WINDOWS) ? '.exe' : nil # :text or :graphical depending on the environment @default_gui_mode = if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(os) || (ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?) # assume not remotely connected on macos and windows or unix family :graphical else :text end @url_method = @default_gui_mode @file_illegal_characters = REPLACE_CHARACTER + WINDOWS_FILENAME_INVALID_CHARACTERS nil end |
#open_editor(file_path) ⇒ Object
open a file in an editor
284 285 286 287 288 289 290 291 292 |
# File 'lib/aspera/environment.rb', line 284 def open_editor(file_path) if ENV.key?('EDITOR') self.class.secure_execute(exec: ENV['EDITOR'], args: [file_path.to_s]) elsif @os.eql?(Environment::OS_WINDOWS) self.class.secure_execute(exec: 'notepad.exe', args: [%Q{"#{file_path}"}]) else open_uri_graphical(file_path.to_s) end end |
#open_uri(the_url) ⇒ Object
Allows a user to open a URL if method is :text, then URL is displayed on terminal if method is :graphical, then the URL will be opened with the default browser. this is non blocking
298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/aspera/environment.rb', line 298 def open_uri(the_url) case @url_method when :graphical open_uri_graphical(the_url) when :text case the_url.to_s when /^http/ puts "USER ACTION: please enter this URL in a browser:\n#{the_url.to_s.red}\n" else puts "USER ACTION: open this:\n#{the_url.to_s.red}\n" end else Aspera.error_unexpected_value(@url_method){'URL open method'} end end |
#open_uri_graphical(uri) ⇒ Object
Open a URI in a graphical browser Command must be non blocking
274 275 276 277 278 279 280 281 |
# File 'lib/aspera/environment.rb', line 274 def open_uri_graphical(uri) case @os when Environment::OS_MACOS then return self.class.secure_execute(exec: 'open', args: [uri.to_s]) when Environment::OS_WINDOWS then return self.class.secure_execute(exec: 'start', args: ['explorer', %Q{"#{uri}"}]) when Environment::OS_LINUX then return self.class.secure_execute(exec: 'xdg-open', args: [uri.to_s]) else Assert.error_unexpected_value(os){'no graphical open method'} end end |
#safe_filename_character ⇒ Object
Replacement character for illegal filename characters Can also be used as safe “join” character
315 316 317 318 |
# File 'lib/aspera/environment.rb', line 315 def safe_filename_character return REPLACE_CHARACTER if @file_illegal_characters.nil? || @file_illegal_characters.empty? @file_illegal_characters[0] end |
#sanitized_filename(filename) ⇒ String
Sanitize a filename by replacing illegal characters
323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/aspera/environment.rb', line 323 def sanitized_filename(filename) safe_char = safe_filename_character # Windows does not allow file name: # - with control characters anywhere # - ending with space or dot filename = filename.gsub(/[\x00-\x1F\x7F]/, safe_char) filename = filename.chop while filename.end_with?(' ', '.') if @file_illegal_characters&.size.to_i >= 2 # replace all illegal characters with safe_char filename = filename.tr(@file_illegal_characters[1..-1], safe_char) end # ensure only one safe_char is used at a time return filename.gsub(/#{Regexp.escape(safe_char)}+/, safe_char).chomp(safe_char) end |