diff --git a/pkg/shaman/server.go b/pkg/shaman/server.go index 61a2649e..02e6b71f 100644 --- a/pkg/shaman/server.go +++ b/pkg/shaman/server.go @@ -109,7 +109,8 @@ func checkPlatformSymlinkSupport() { Str("os", runtime.GOOS). Str("arch", runtime.GOARCH). Str("osDetail", osDetail). - Msg("this platform does not reliably support symbolic links; using the Shaman system is not recommended") + Msg("this platform does not reliably support symbolic links, " + + "see https://flamenco.blender.org/usage/shared-storage/shaman/#requirements") } // Go starts goroutines for background operations. diff --git a/pkg/sysinfo/sysinfo_windows.go b/pkg/sysinfo/sysinfo_windows.go index 85b5e2f6..4108f22a 100644 --- a/pkg/sysinfo/sysinfo_windows.go +++ b/pkg/sysinfo/sysinfo_windows.go @@ -3,34 +3,23 @@ package sysinfo // SPDX-License-Identifier: GPL-3.0-or-later import ( + "encoding/binary" "fmt" + "syscall" + "unsafe" "github.com/rs/zerolog/log" + "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) -// editionCanSymlink maps the EditionID key in the registry to a boolean indicating whether -// symlinks are available. -var editionCanSymlink = map[string]bool{ - "Core": false, // Windows Home misses the tool to allow symlinking to a user. - "Professional": true, // Still requires explicit right to be assigned to the user. -} - // canSymlink tries to determine whether the running system can use symbolic // links, based on information from the Windows registry. func canSymlink() (bool, error) { - editionID, err := windowsEditionID() - if err != nil { - return false, fmt.Errorf("determining edition of Windows: %w", err) - } - - canSymlink, found := editionCanSymlink[editionID] - if !found { - log.Warn().Str("editionID", editionID).Msg("unknown Windows edition, assuming it can use symlinks") + if isDeveloperModeActive() { return true, nil } - - return canSymlink, nil + return hasSystemPrivilege("SeCreateSymbolicLinkPrivilege") } func description() (string, error) { @@ -78,3 +67,87 @@ func registryReadString(keyPath, valueName string) (string, error) { return value, nil } + +// isDeveloperModeActive checks whether or not the developer mode is active on Windows 10. +// Returns false for prior Windows versions. +// see https://docs.microsoft.com/en-us/windows/uwp/get-started/enable-your-device-for-development +// Copied from https://github.com/golang/go/pull/24307/files +func isDeveloperModeActive() bool { + key, err := registry.OpenKey( + registry.LOCAL_MACHINE, + `SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock`, + registry.READ) + if err != nil { + return false + } + + val, _, err := key.GetIntegerValue("AllowDevelopmentWithoutDevLicense") + if err != nil { + return false + } + + return val != 0 +} + +// hasSystemPrivilege checks whether the user has the +// SeCreateSymbolicLinkPrivilege, which is necessary to create symbolic links. +func hasSystemPrivilege(privilegeName string) (bool, error) { + // This doesn't fail, and just returns -1. The Microsoft docs still recommend + // calling this function, though, instead of just hard-coding the -1 value. + hProcess := windows.CurrentProcess() + + // Open a process token, necessary for the subsequent calls. + var processToken windows.Token + err := windows.OpenProcessToken(hProcess, windows.TOKEN_READ, &processToken) + if err != nil { + return false, fmt.Errorf("calling OpenProcessToken: %w", err) + } + defer func() { + _ = processToken.Close() + }() + + privilegeNameU16, err := syscall.UTF16PtrFromString(privilegeName) + if err != nil { + return false, fmt.Errorf("invalid privilege name %q: %w", privilegeName, err) + } + + // Look up the LUID for the privilege. + var privilegeLUID windows.LUID + err = windows.LookupPrivilegeValue(nil, privilegeNameU16, &privilegeLUID) + if err != nil { + return false, fmt.Errorf("calling LookupPrivilegeValue: %w", err) + } + + // Get the size of the buffer needed for the actual data. + var TokenPrivileges uint32 = 3 + var bufferSize uint32 + err = windows.GetTokenInformation(processToken, TokenPrivileges, nil, 0, &bufferSize) + if errno, ok := err.(syscall.Errno); !ok || errno != 122 { + return false, fmt.Errorf("unexpected error from first GetTokenInformation call: %w", err) + } + + // Get the list of user's privileges. + buffer := make([]byte, bufferSize) + err = windows.GetTokenInformation(processToken, TokenPrivileges, &buffer[0], bufferSize, &bufferSize) + if err != nil { + return false, fmt.Errorf("unexpected error from second GetTokenInformation call: %w", err) + } + + // Decode the privileges. + privCount := int(binary.LittleEndian.Uint32(buffer)) + offset := int(unsafe.Sizeof(uint32(0))) + structSize := int(unsafe.Sizeof(windows.LUIDAndAttributes{})) + + for i := 0; i < privCount; i++ { + structPtr := &buffer[offset+structSize*i] + luidAndAttr := *(*windows.LUIDAndAttributes)(unsafe.Pointer(structPtr)) + if luidAndAttr.Luid != privilegeLUID { + continue + } + + log.Debug().Str("privilege", privilegeName).Msg("found privilege") + return true, nil + } + + return false, nil +} diff --git a/web/project-website/content/usage/shared-storage/shaman.md b/web/project-website/content/usage/shared-storage/shaman.md index 6901919d..7406ef0c 100644 --- a/web/project-website/content/usage/shared-storage/shaman.md +++ b/web/project-website/content/usage/shared-storage/shaman.md @@ -53,13 +53,33 @@ Because of the use of *symbolic links* (also known as *symlinks*), using Shaman is only possible on systems that support those. These should be supported by the computers running Flamenco Manager and Workers. - ### Windows The Shaman storage system uses _symbolic links_. On Windows the creation of -symbolic links requires a change in security policy. Unfortunately, *Home* -editions of Windows do not have a policy editor, but the freely available -[Polsedit][polsedit] can be used on these editions. +symbolic links requires a change in security policy. This can be done as +follows: + +{{< tabs "shaman-windows" >}} +{{< tab "Windows Home / Core" >}} + +On Windows Home (also known as "core"), you'll need to enable Developer Mode: + +1. Press the Windows key, type "*Developer settings*", and click Open or press + Enter. +2. Click the slider under "*Developer Mode*" to turn it ON. + +See [Developer Mode][devmode] for more information, including some security implications. + +Alternatively you can use the freely available [Polsedit][polsedit] to enable +the *Create Symbolic Links* security policy. + +[devmode]: https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development +[polsedit]: https://www.southsoftware.com/polsedit.html + +{{< /tab >}} +{{< tab "Windows Pro / Enterprise" >}} + +On Windows Pro & Enterprise you need to enable a security policy. 1. Press Win+R, in the popup type `secpol.msc`. Then click OK. 2. In the _Local Security Policy_ window that opens, go to _Security Settings_ > _Local Policies_ > _User Rights Assignment_. @@ -67,8 +87,12 @@ editions of Windows do not have a policy editor, but the freely available 4. Double-click the item and add yourself (or the user running Flamenco Manager or the whole users group) to the list. 5. Log out & back in again, or reboot the machine. -[polsedit]: https://www.southsoftware.com/polsedit.html +For more info see [the Microsoft documentation][secpol]. +[secpol]: https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links + +{{< /tab >}} +{{< /tabs >}} ### Linux