Skip to content

Commit

Permalink
Detect symlink creation capability on Windows (#617)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored Feb 22, 2024
1 parent fe754a0 commit 80b038a
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 13 deletions.
13 changes: 0 additions & 13 deletions src/cli.cr
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,4 @@ rescue ex : Shards::ParseError
rescue ex : Shards::Error
Shards::Log.error { ex.message }
exit 1
rescue exc : File::AccessDeniedError
{% if flag?(:windows) %}
if exc.os_error == WinError::ERROR_PRIVILEGE_NOT_HELD && exc.message.try &.starts_with?("Error creating symlink")
Shards::Log.error { <<-TXT }
#{exc}
Shards needs symlinks to work. Please make sure to enable developer mode:
https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
TXT
exit 1
end
{% end %}
raise exc
end
12 changes: 12 additions & 0 deletions src/commands/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ module Shards
end
end

def check_symlink_privilege
{% if flag?(:win32) %}
return if Shards::Helpers.developer_mode?
return if Shards::Helpers.privilege_enabled?("SeCreateSymbolicLinkPrivilege")

raise Shards::Error.new(<<-EOS)
Shards needs symlinks to work. Please enable Developer Mode, or run Shards with elevated rights:
https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
EOS
{% end %}
end

def touch_install_path
Dir.mkdir_p(Shards.install_path)
File.touch(Shards.install_path)
Expand Down
1 change: 1 addition & 0 deletions src/commands/install.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Shards
if Shards.frozen? && !lockfile?
raise Error.new("Missing shard.lock")
end
check_symlink_privilege

Log.info { "Resolving dependencies" }

Expand Down
2 changes: 2 additions & 0 deletions src/commands/lock.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module Shards
module Commands
class Lock < Command
def run(shards : Array(String), print = false, update = false)
check_symlink_privilege

Log.info { "Resolving dependencies" }

solver = MolinilloSolver.new(spec, override)
Expand Down
2 changes: 2 additions & 0 deletions src/commands/outdated.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module Shards
@output = IO::Memory.new

def run(@prereleases = false)
check_symlink_privilege

return unless has_dependencies?

Log.info { "Resolving dependencies" }
Expand Down
2 changes: 2 additions & 0 deletions src/commands/update.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module Shards
module Commands
class Update < Command
def run(shards : Array(String))
check_symlink_privilege

Log.info { "Resolving dependencies" }

solver = MolinilloSolver.new(spec, override)
Expand Down
91 changes: 91 additions & 0 deletions src/helpers.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
{% if flag?(:win32) %}
lib LibC
struct LUID
lowPart : DWORD
highPart : Long
end

struct LUID_AND_ATTRIBUTES
luid : LUID
attributes : DWORD
end

struct TOKEN_PRIVILEGES
privilegeCount : DWORD
privileges : LUID_AND_ATTRIBUTES[1]
end

TOKEN_QUERY = 0x0008
TOKEN_ADJUST_PRIVILEGES = 0x0020

TokenPrivileges = 3

SE_PRIVILEGE_ENABLED = 0x00000002_u32

fun OpenProcessToken(processHandle : HANDLE, desiredAccess : DWORD, tokenHandle : HANDLE*) : BOOL
fun GetTokenInformation(tokenHandle : HANDLE, tokenInformationClass : Int, tokenInformation : Void*, tokenInformationLength : DWORD, returnLength : DWORD*) : BOOL
fun LookupPrivilegeValueW(lpSystemName : LPWSTR, lpName : LPWSTR, lpLuid : LUID*) : BOOL
fun AdjustTokenPrivileges(tokenHandle : HANDLE, disableAllPrivileges : BOOL, newState : TOKEN_PRIVILEGES*, bufferLength : DWORD, previousState : TOKEN_PRIVILEGES*, returnLength : DWORD*) : BOOL
end
{% end %}

module Shards::Helpers
def self.rm_rf(path : String) : Nil
# TODO: delete this and use https://github.com/crystal-lang/crystal/pull/9903
Expand Down Expand Up @@ -32,4 +63,64 @@ module Shards::Helpers
name
{% end %}
end

def self.privilege_enabled?(privilege_name : String) : Bool
{% if flag?(:win32) %}
if LibC.LookupPrivilegeValueW(nil, privilege_name.to_utf16, out privilege_luid) == 0
return false
end

# if the process token already has the privilege, and the privilege is already enabled,
# we don't need to do anything else
if LibC.OpenProcessToken(LibC.GetCurrentProcess, LibC::TOKEN_QUERY, out token) != 0
begin
LibC.GetTokenInformation(token, LibC::TokenPrivileges, nil, 0, out len)
buf = Pointer(UInt8).malloc(len).as(LibC::TOKEN_PRIVILEGES*)
LibC.GetTokenInformation(token, LibC::TokenPrivileges, buf, len, out _)
privileges = Slice.new(pointerof(buf.value.@privileges).as(LibC::LUID_AND_ATTRIBUTES*), buf.value.privilegeCount)
# if the process token doesn't have the privilege, there is no way
# `AdjustTokenPrivileges` could grant or enable it
privilege = privileges.find(&.luid.== privilege_luid)
return false unless privilege
return true if privilege.attributes.bits_set?(LibC::SE_PRIVILEGE_ENABLED)
ensure
LibC.CloseHandle(token)
end
end

if LibC.OpenProcessToken(LibC.GetCurrentProcess, LibC::TOKEN_ADJUST_PRIVILEGES, out adjust_token) != 0
new_privileges = LibC::TOKEN_PRIVILEGES.new(
privilegeCount: 1,
privileges: StaticArray[
LibC::LUID_AND_ATTRIBUTES.new(
luid: privilege_luid,
attributes: LibC::SE_PRIVILEGE_ENABLED,
),
],
)
if LibC.AdjustTokenPrivileges(adjust_token, 0, pointerof(new_privileges), 0, nil, nil) != 0
return true if WinError.value.error_success?
end
end

false
{% else %}
raise NotImplementedError.new("Shards::Helpers.privilege_enabled?")
{% end %}
end

def self.developer_mode? : Bool
{% if flag?(:win32) %}
key = %q(SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock).to_utf16
!!Crystal::System::WindowsRegistry.open?(LibC::HKEY_LOCAL_MACHINE, key) do |handle|
value = uninitialized LibC::DWORD
name = "AllowDevelopmentWithoutDevLicense".to_utf16
bytes = Slice.new(pointerof(value), 1).to_unsafe_bytes
type, len = Crystal::System::WindowsRegistry.get_raw(handle, name, bytes) || return false
return type.dword? && len == sizeof(typeof(value)) && value != 0
end
{% else %}
raise NotImplementedError.new("Shards::Helpers.developer_mode?")
{% end %}
end
end

0 comments on commit 80b038a

Please sign in to comment.