-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
executable_path.cr
183 lines (162 loc) · 5.26 KB
/
executable_path.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# Reference:
# - https://github.com/gpakosz/whereami/blob/master/src/whereami.c
# - http://stackoverflow.com/questions/1023306/finding-current-executables-path-without-proc-self-exe
class Process
PATH_DELIMITER = {% if flag?(:windows) %} ';' {% else %} ':' {% end %}
# :nodoc:
INITIAL_PATH = ENV["PATH"]?
# :nodoc:
#
# Working directory at program start. Nil if working directory does not exist.
#
# Used for `Exception::CallStack::CURRENT_DIR`
# and `Process.executable_path_impl` on openbsd.
INITIAL_PWD = begin
Dir.current
rescue File::NotFoundError
nil
end
# Returns an absolute path to the executable file of the currently running
# program. This is in opposition to `PROGRAM_NAME` which may be a relative or
# absolute path, just the executable file name or a symlink.
#
# The executable path will be canonicalized (all symlinks and relative paths
# will be expanded).
#
# Returns `nil` if the file can't be found.
def self.executable_path : String?
if executable = executable_path_impl
begin
File.realpath(executable)
rescue File::Error
end
end
end
private def self.is_executable_file?(path)
unless File.info?(path, follow_symlinks: true).try &.file?
return false
end
{% if flag?(:win32) %}
# This is *not* a temporary stub.
# Windows doesn't have "executable" metadata for files, so it also doesn't have files that are "not executable".
true
{% else %}
File.executable?(path)
{% end %}
end
# Searches an executable, checking for an absolute path, a path relative to
# *pwd* or absolute path, then eventually searching in directories declared
# in *path*.
def self.find_executable(name : Path | String, path : String? = ENV["PATH"]?, pwd : Path | String = Dir.current) : String?
find_executable_possibilities(Path.new(name), path, pwd) do |p|
if is_executable_file?(p)
return p.to_s
end
end
nil
end
private def self.find_executable_possibilities(name, path, pwd)
return if name.to_s.empty?
{% if flag?(:win32) %}
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw#parameters
# > If the file name does not contain an extension, .exe is appended.
# See find_executable_spec.cr for cases this needs to match, based on CreateProcessW behavior.
basename = name.ends_with_separator? ? "" : name.basename
basename = "" if basename == name.anchor.to_s
if (basename.empty? ? !name.anchor : !basename.includes?("."))
name = Path.new("#{name}.exe")
end
{% end %}
if name.absolute?
yield name
end
# check if the name includes a separator
count_parts = 0
name.each_part do
count_parts += 1
break if count_parts > 1
end
has_separator = (count_parts > 1)
if {{ flag?(:win32) }} || has_separator
yield name.expand(pwd)
end
if path && !has_separator
path.split(PATH_DELIMITER).each do |path_entry|
yield Path.new(path_entry, name)
end
end
end
end
{% if flag?(:darwin) %}
lib LibC
PATH_MAX = 1024
fun _NSGetExecutablePath(buf : Char*, bufsize : UInt32*) : Int
end
class Process
private def self.executable_path_impl
buf = GC.malloc_atomic(LibC::PATH_MAX).as(UInt8*)
size = LibC::PATH_MAX.to_u32
if LibC._NSGetExecutablePath(buf, pointerof(size)) == -1
buf = GC.malloc_atomic(size).as(UInt8*)
return nil if LibC._NSGetExecutablePath(buf, pointerof(size)) == -1
end
String.new(buf)
end
end
{% elsif flag?(:freebsd) || flag?(:dragonfly) %}
require "c/sysctl"
class Process
private def self.executable_path_impl
mib = Int32[LibC::CTL_KERN, LibC::KERN_PROC, LibC::KERN_PROC_PATHNAME, -1]
buf = GC.malloc_atomic(LibC::PATH_MAX).as(UInt8*)
size = LibC::SizeT.new(LibC::PATH_MAX)
if LibC.sysctl(mib, 4, buf, pointerof(size), nil, 0) == 0
String.new(buf, size - 1)
end
end
end
{% elsif flag?(:netbsd) %}
require "c/sysctl"
class Process
private def self.executable_path_impl
mib = Int32[LibC::CTL_KERN, LibC::KERN_PROC_ARGS, -1, LibC::KERN_PROC_PATHNAME]
buf = GC.malloc_atomic(LibC::PATH_MAX).as(UInt8*)
size = LibC::SizeT.new(LibC::PATH_MAX)
if LibC.sysctl(mib, 4, buf, pointerof(size), nil, 0) == 0
String.new(buf, size - 1)
end
end
end
{% elsif flag?(:linux) %}
class Process
private def self.executable_path_impl
"/proc/self/exe"
end
end
{% elsif flag?(:win32) %}
require "crystal/system/windows"
require "c/libloaderapi"
class Process
private def self.executable_path_impl
Crystal::System.retry_wstr_buffer do |buffer, small_buf|
len = LibC.GetModuleFileNameW(nil, buffer, buffer.size)
if 0 < len < buffer.size
break String.from_utf16(buffer[0, len])
elsif small_buf && len == buffer.size
next 32767 # big enough. 32767 is the maximum total path length of UNC path.
else
break nil
end
end
end
end
{% else %}
# openbsd, ...
class Process
private def self.executable_path_impl
if pwd = INITIAL_PWD
find_executable(PROGRAM_NAME, INITIAL_PATH, pwd)
end
end
end
{% end %}