Skip to content

Commit 80b038a

Browse files
authored
Detect symlink creation capability on Windows (#617)
1 parent fe754a0 commit 80b038a

File tree

7 files changed

+110
-13
lines changed

7 files changed

+110
-13
lines changed

src/cli.cr

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,17 +162,4 @@ rescue ex : Shards::ParseError
162162
rescue ex : Shards::Error
163163
Shards::Log.error { ex.message }
164164
exit 1
165-
rescue exc : File::AccessDeniedError
166-
{% if flag?(:windows) %}
167-
if exc.os_error == WinError::ERROR_PRIVILEGE_NOT_HELD && exc.message.try &.starts_with?("Error creating symlink")
168-
Shards::Log.error { <<-TXT }
169-
#{exc}
170-
171-
Shards needs symlinks to work. Please make sure to enable developer mode:
172-
https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
173-
TXT
174-
exit 1
175-
end
176-
{% end %}
177-
raise exc
178165
end

src/commands/command.cr

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ module Shards
9292
end
9393
end
9494

95+
def check_symlink_privilege
96+
{% if flag?(:win32) %}
97+
return if Shards::Helpers.developer_mode?
98+
return if Shards::Helpers.privilege_enabled?("SeCreateSymbolicLinkPrivilege")
99+
100+
raise Shards::Error.new(<<-EOS)
101+
Shards needs symlinks to work. Please enable Developer Mode, or run Shards with elevated rights:
102+
https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
103+
EOS
104+
{% end %}
105+
end
106+
95107
def touch_install_path
96108
Dir.mkdir_p(Shards.install_path)
97109
File.touch(Shards.install_path)

src/commands/install.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Shards
88
if Shards.frozen? && !lockfile?
99
raise Error.new("Missing shard.lock")
1010
end
11+
check_symlink_privilege
1112

1213
Log.info { "Resolving dependencies" }
1314

src/commands/lock.cr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ module Shards
55
module Commands
66
class Lock < Command
77
def run(shards : Array(String), print = false, update = false)
8+
check_symlink_privilege
9+
810
Log.info { "Resolving dependencies" }
911

1012
solver = MolinilloSolver.new(spec, override)

src/commands/outdated.cr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ module Shards
99
@output = IO::Memory.new
1010

1111
def run(@prereleases = false)
12+
check_symlink_privilege
13+
1214
return unless has_dependencies?
1315

1416
Log.info { "Resolving dependencies" }

src/commands/update.cr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ module Shards
55
module Commands
66
class Update < Command
77
def run(shards : Array(String))
8+
check_symlink_privilege
9+
810
Log.info { "Resolving dependencies" }
911

1012
solver = MolinilloSolver.new(spec, override)

src/helpers.cr

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1+
{% if flag?(:win32) %}
2+
lib LibC
3+
struct LUID
4+
lowPart : DWORD
5+
highPart : Long
6+
end
7+
8+
struct LUID_AND_ATTRIBUTES
9+
luid : LUID
10+
attributes : DWORD
11+
end
12+
13+
struct TOKEN_PRIVILEGES
14+
privilegeCount : DWORD
15+
privileges : LUID_AND_ATTRIBUTES[1]
16+
end
17+
18+
TOKEN_QUERY = 0x0008
19+
TOKEN_ADJUST_PRIVILEGES = 0x0020
20+
21+
TokenPrivileges = 3
22+
23+
SE_PRIVILEGE_ENABLED = 0x00000002_u32
24+
25+
fun OpenProcessToken(processHandle : HANDLE, desiredAccess : DWORD, tokenHandle : HANDLE*) : BOOL
26+
fun GetTokenInformation(tokenHandle : HANDLE, tokenInformationClass : Int, tokenInformation : Void*, tokenInformationLength : DWORD, returnLength : DWORD*) : BOOL
27+
fun LookupPrivilegeValueW(lpSystemName : LPWSTR, lpName : LPWSTR, lpLuid : LUID*) : BOOL
28+
fun AdjustTokenPrivileges(tokenHandle : HANDLE, disableAllPrivileges : BOOL, newState : TOKEN_PRIVILEGES*, bufferLength : DWORD, previousState : TOKEN_PRIVILEGES*, returnLength : DWORD*) : BOOL
29+
end
30+
{% end %}
31+
132
module Shards::Helpers
233
def self.rm_rf(path : String) : Nil
334
# TODO: delete this and use https://github.com/crystal-lang/crystal/pull/9903
@@ -32,4 +63,64 @@ module Shards::Helpers
3263
name
3364
{% end %}
3465
end
66+
67+
def self.privilege_enabled?(privilege_name : String) : Bool
68+
{% if flag?(:win32) %}
69+
if LibC.LookupPrivilegeValueW(nil, privilege_name.to_utf16, out privilege_luid) == 0
70+
return false
71+
end
72+
73+
# if the process token already has the privilege, and the privilege is already enabled,
74+
# we don't need to do anything else
75+
if LibC.OpenProcessToken(LibC.GetCurrentProcess, LibC::TOKEN_QUERY, out token) != 0
76+
begin
77+
LibC.GetTokenInformation(token, LibC::TokenPrivileges, nil, 0, out len)
78+
buf = Pointer(UInt8).malloc(len).as(LibC::TOKEN_PRIVILEGES*)
79+
LibC.GetTokenInformation(token, LibC::TokenPrivileges, buf, len, out _)
80+
privileges = Slice.new(pointerof(buf.value.@privileges).as(LibC::LUID_AND_ATTRIBUTES*), buf.value.privilegeCount)
81+
# if the process token doesn't have the privilege, there is no way
82+
# `AdjustTokenPrivileges` could grant or enable it
83+
privilege = privileges.find(&.luid.== privilege_luid)
84+
return false unless privilege
85+
return true if privilege.attributes.bits_set?(LibC::SE_PRIVILEGE_ENABLED)
86+
ensure
87+
LibC.CloseHandle(token)
88+
end
89+
end
90+
91+
if LibC.OpenProcessToken(LibC.GetCurrentProcess, LibC::TOKEN_ADJUST_PRIVILEGES, out adjust_token) != 0
92+
new_privileges = LibC::TOKEN_PRIVILEGES.new(
93+
privilegeCount: 1,
94+
privileges: StaticArray[
95+
LibC::LUID_AND_ATTRIBUTES.new(
96+
luid: privilege_luid,
97+
attributes: LibC::SE_PRIVILEGE_ENABLED,
98+
),
99+
],
100+
)
101+
if LibC.AdjustTokenPrivileges(adjust_token, 0, pointerof(new_privileges), 0, nil, nil) != 0
102+
return true if WinError.value.error_success?
103+
end
104+
end
105+
106+
false
107+
{% else %}
108+
raise NotImplementedError.new("Shards::Helpers.privilege_enabled?")
109+
{% end %}
110+
end
111+
112+
def self.developer_mode? : Bool
113+
{% if flag?(:win32) %}
114+
key = %q(SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock).to_utf16
115+
!!Crystal::System::WindowsRegistry.open?(LibC::HKEY_LOCAL_MACHINE, key) do |handle|
116+
value = uninitialized LibC::DWORD
117+
name = "AllowDevelopmentWithoutDevLicense".to_utf16
118+
bytes = Slice.new(pointerof(value), 1).to_unsafe_bytes
119+
type, len = Crystal::System::WindowsRegistry.get_raw(handle, name, bytes) || return false
120+
return type.dword? && len == sizeof(typeof(value)) && value != 0
121+
end
122+
{% else %}
123+
raise NotImplementedError.new("Shards::Helpers.developer_mode?")
124+
{% end %}
125+
end
35126
end

0 commit comments

Comments
 (0)