Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b7fbc1e
powershell version
AdRiley Apr 2, 2026
485f14b
logging
AdRiley Apr 2, 2026
cd9dd23
fix
AdRiley Apr 2, 2026
0d99e30
fix
AdRiley Apr 2, 2026
3e44d2b
pretty
AdRiley Apr 2, 2026
b485036
more logs
AdRiley Apr 2, 2026
df07290
jvm
AdRiley Apr 3, 2026
1923370
Switch to wide versions of windows APIs
AdRiley Apr 3, 2026
5fee9d3
Revert "Switch to wide versions of windows APIs"
AdRiley Apr 3, 2026
04091fc
Revert "jvm"
AdRiley Apr 3, 2026
4fc3ae2
Try base64 encoding
jdunkerley Apr 7, 2026
66786c7
Minimal powershell - does it work?
jdunkerley Apr 7, 2026
dbacec7
Does need the encoding bit.
jdunkerley Apr 7, 2026
decb222
Reapply "Switch to wide versions of windows APIs"
jdunkerley Apr 7, 2026
b34ac96
Possibly working with W version...
jdunkerley Apr 8, 2026
4146ba9
Add unicode tests.
jdunkerley Apr 8, 2026
b4d2584
Use Unicode function to get command line arguments.
jdunkerley Apr 14, 2026
b3fdb6d
WIP
jdunkerley Apr 15, 2026
387d0c3
First fully working version!
jdunkerley Apr 15, 2026
a5d617e
Remove some of the diagnostics.
jdunkerley Apr 15, 2026
6593697
Move diagnostics in Java to Logger.
jdunkerley Apr 15, 2026
848727f
Use Windows API for parsing out command line args.
jdunkerley Apr 15, 2026
be2429b
Last bits of Java tidy up.
jdunkerley Apr 15, 2026
ee7f518
Remove unused.
jdunkerley Apr 15, 2026
2009d89
More logging tidy up.
jdunkerley Apr 15, 2026
e9a95fb
More logging tidy up.
jdunkerley Apr 15, 2026
25895bd
Java format.
jdunkerley Apr 15, 2026
13df435
Java format.
jdunkerley Apr 15, 2026
6ea59ce
Reset last bits.
jdunkerley Apr 15, 2026
3c9ace9
Prettier.
jdunkerley Apr 15, 2026
1f8dab8
Remove console.debug statements.
jdunkerley Apr 15, 2026
8929e6d
PR comments (Part 1).
jdunkerley Apr 15, 2026
5752cab
Use unmanaged.
jdunkerley Apr 16, 2026
d435d83
Drop arg[0] as don't want that in args array.
jdunkerley Apr 16, 2026
382cceb
Arguments at trace.
jdunkerley Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions app/project-manager-shim/src/desktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,19 @@ function getMacOsDocumentsPath(): string {
}

/** Get the path to the `My Documents` Windows directory. */
function getWindowsDocumentsPath(): string | undefined {
function getWindowsDocumentsPath() {
const out = childProcess.spawnSync(
'reg',
'powershell',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we run on Windows that don't have powershell?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been built into Windows since Windows 7 I think - so think ok to assume its there.

[
'query',
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
'/v',
'personal',
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; [Environment]::GetFolderPath("MyDocuments")',
],
{ timeout: CHILD_PROCESS_TIMEOUT },
)

if (out.error !== undefined) {
return
} else {
const stdoutString = out.stdout.toString()
return stdoutString.split(/\s\s+/)[4]
if (out.error) {
console.warn(`getWindowsDocumentsPath.error: ${out.error.message}`)
return undefined
}

return out.stdout.toString().trim() || undefined
}
3 changes: 3 additions & 0 deletions engine/runner/src/main/java/org/enso/runner/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.enso.libraryupload.LibraryUploader.UploadFailedError;
import org.enso.logger.Converter;
import org.enso.logger.ObservedMessage;
import org.enso.os.environment.Arguments;
import org.enso.pkg.Contact;
import org.enso.pkg.PackageManager;
import org.enso.pkg.PackageManager$;
Expand Down Expand Up @@ -1124,6 +1125,8 @@ private URI parseUri(String string) {
* @param args the command line arguments
*/
public static void main(String[] args) throws Exception {
// Handle an issue with Windows arguments containing UTF-16 characters
args = Arguments.getCurrent().alterArgs(args);
new Main().launch(args);
}

Expand Down
3 changes: 3 additions & 0 deletions engine/runner/src/main/java/org/enso/runner/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ static String adjustCwdToProject(String fileToRun) {
if (!ImageInfo.inImageRuntimeCode()) {
return System.getProperty("enso.user.dir");
}

var nativeApi = WorkingDirectory.getInstance();
var projectRoot = nativeApi.findProjectRoot(fileToRun);
if (projectRoot != null) {
Expand All @@ -222,6 +223,8 @@ static String adjustCwdToProject(String fileToRun) {
var dirChanged = nativeApi.changeWorkingDir(parentDir);
if (!dirChanged) {
LOGGER.error("Cannot change working directory to {}", parentDir);
} else {
LOGGER.debug("Changed working directory to {}", parentDir);
}
}
return curDir;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.enso.os.environment;

import org.enso.common.Platform;

public sealed interface Arguments permits WindowsArguments, LinuxArguments {
static Arguments getCurrent() {
return switch (Platform.getOperatingSystem()) {
case LINUX, MACOS -> LinuxArguments.INSTANCE;
case WINDOWS -> WindowsArguments.INSTANCE;
};
}

String[] alterArgs(String[] originalArgs);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.enso.os.environment;

final class LinuxArguments implements Arguments {
static final LinuxArguments INSTANCE = new LinuxArguments();

private LinuxArguments() {}

@Override
public String[] alterArgs(String[] originalArgs) {
return originalArgs;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.enso.os.environment;

import java.util.List;
import org.enso.common.Platform;
import org.graalvm.nativeimage.ImageInfo;
import org.graalvm.nativeimage.StackValue;
import org.graalvm.nativeimage.c.CContext;
import org.graalvm.nativeimage.c.function.CFunction;
import org.graalvm.nativeimage.c.struct.CPointerTo;
import org.graalvm.nativeimage.c.type.CIntPointer;
import org.graalvm.nativeimage.c.type.CTypeConversion;
import org.graalvm.word.PointerBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@CContext(WindowsArguments.Directives.class)
final class WindowsArguments implements Arguments {
static final WindowsArguments INSTANCE = new WindowsArguments();

private WindowsArguments() {}

@Override
public String[] alterArgs(String[] originalArgs) {
if (!ImageInfo.inImageRuntimeCode()) {
return originalArgs;
}

return readCommandLineArgs();
}

private static final Logger LOGGER = LoggerFactory.getLogger(WindowsArguments.class);

private static final int WCHAR_SIZE = 2;

private static String[] readCommandLineArgs() {
var cmd = GetCommandLineW();

CIntPointer numOfArgs = StackValue.get(Long.BYTES);

WCharPointerPointer args = CommandLineToArgvW(cmd, numOfArgs);
try {
var numArgs = numOfArgs.read();

var results = new String[numArgs - 1];
for (var i = 0; i < results.length; i++) {
var arg = args.read(i + 1);
results[i] = toJavaString(arg);
LOGGER.debug("Read command line argument {}: {}", i, results[i]);
}

return results;
} finally {
LocalFree(args);
}
}

private static String toJavaString(WCharPointer arg) {
return CTypeConversion.asByteBuffer(arg, wcslen(arg) * WCHAR_SIZE)
.order(java.nio.ByteOrder.LITTLE_ENDIAN)
.asCharBuffer()
.toString();
}

@CPointerTo(nameOfCType = "wchar_t")
private interface WCharPointer extends PointerBase {}

@CPointerTo(WCharPointer.class)
private interface WCharPointerPointer extends PointerBase {
WCharPointer read(int index);
}

@CFunction
private static native WCharPointer GetCommandLineW();

@CFunction
private static native WCharPointerPointer CommandLineToArgvW(
WCharPointer cmdLine, CIntPointer numArgsOut);

@CFunction
private static native int wcslen(WCharPointer str);

@CFunction
private static native void LocalFree(PointerBase p);

static final class Directives implements CContext.Directives {
@Override
public boolean isInConfiguration() {
return Platform.getOperatingSystem().isWindows();
}

@Override
public List<String> getHeaderFiles() {
return List.of("<windows.h>", "<wchar.h>");
}

@Override
public List<String> getLibraries() {
return List.of("Kernel32", "Shell32");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,71 @@
package org.enso.os.environment.chdir;

import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.enso.common.Platform;
import org.graalvm.nativeimage.UnmanagedMemory;
import org.graalvm.nativeimage.c.CContext;
import org.graalvm.nativeimage.c.function.CFunction;
import org.graalvm.nativeimage.c.type.CCharPointer;
import org.graalvm.nativeimage.c.type.CTypeConversion;
import org.graalvm.word.PointerBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@CContext(WindowsWorkingDirectory.Directives.class)
final class WindowsWorkingDirectory extends WorkingDirectory {

static final WindowsWorkingDirectory INSTANCE = new WindowsWorkingDirectory();
private static final Logger LOGGER = LoggerFactory.getLogger(WindowsWorkingDirectory.class);

// WChars in windows are 2 bytes
private static final int WCHAR_SIZE = 2;

// Windows MAX_PATH is 260 and MAX_PATH_WIDE is 32767
private static final int MAX_LENGTH = 32767;

private static String wcharPtrAsString(PointerBase buffer, int length) {
return CTypeConversion.asByteBuffer(buffer, length * WCHAR_SIZE)
.order(ByteOrder.LITTLE_ENDIAN)
.asCharBuffer()
.toString();
}

private static PointerBase stringAsWCharPtr(String input) {
var bytes = input.getBytes(StandardCharsets.UTF_16LE);
var buffer = UnmanagedMemory.malloc(bytes.length + 2);
CTypeConversion.asByteBuffer(buffer, bytes.length + 2)
.order(ByteOrder.LITTLE_ENDIAN)
.put(bytes)
.put(new byte[] {0, 0});
return buffer;
}

@Override
public String currentWorkingDir() {
byte[] buf = new byte[4096];
String path;
try (var ptrHolder = CTypeConversion.toCBytes(buf)) {
var ptr = ptrHolder.get();
var ret = GetCurrentDirectoryA(4096, ptr);
if (ret == 0) {
LOGGER.error("GetCurrentDirectory failed with {}", ret);
var buffer = UnmanagedMemory.malloc(MAX_LENGTH * WCHAR_SIZE);
try {
int length = GetCurrentDirectoryW(MAX_LENGTH, buffer);
if (length == 0 || length == MAX_LENGTH) {
LOGGER.error("GetCurrentDirectory failed with length {}", length);
return null;
} else {
path = new String(buf);
}

var result = wcharPtrAsString(buffer, length);
LOGGER.debug("Current working directory is {}", result);
return result;
} finally {
UnmanagedMemory.free(buffer);
}
return path.trim();
}

@Override
public boolean changeWorkingDir(String path) {
path = normalizeSlashes(path);

try (var cPath = CTypeConversion.toCString(path)) {
var res = SetCurrentDirectoryA(cPath.get());
var buffer = stringAsWCharPtr(path);
try {
var res = SetCurrentDirectoryW(buffer);
if (res == 0) {
LOGGER.error("SetCurrrentDirectory to {} failed with {}", path, res);
return false;
Expand All @@ -45,6 +74,8 @@ public boolean changeWorkingDir(String path) {
} catch (Throwable t) {
LOGGER.error("Cannot change working directory to " + path + " on Windows", t);
throw t;
} finally {
UnmanagedMemory.free(buffer);
}
}

Expand All @@ -53,12 +84,15 @@ public boolean exists(String dir, String file) {
dir = normalizeSlashes(dir);
file = normalizeSlashes(file);
var full = dir + Platform.separatorChar() + file;
try (var cPath = CTypeConversion.toCString(full)) {
var res = PathFileExistsA(cPath.get());
var buffer = stringAsWCharPtr(full);
try {
var res = PathFileExistsW(buffer);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's use W version of the API methods.

return res != 0;
} catch (Throwable t) {
LOGGER.error("Cannot check if {} exists on Windows", full, t);
return false;
} finally {
UnmanagedMemory.free(buffer);
}
}

Expand All @@ -77,23 +111,23 @@ private static String normalizeSlashes(String path) {
* docs</a>
*/
@CFunction
static native int GetCurrentDirectoryA(int nBufferLength, CCharPointer lpBuffer);
static native int GetCurrentDirectoryW(int nBufferLength, PointerBase lpBuffer);

/**
* <a
* href="https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setcurrentdirectory">Official
* docs</a>
*/
@CFunction
static native int SetCurrentDirectoryA(CCharPointer lpPathName);
static native int SetCurrentDirectoryW(PointerBase lpPathName);

/**
* <a
* href="https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-pathfileexistsa">Official
* docs</a>
*/
@CFunction
static native int PathFileExistsA(CCharPointer pszPath);
static native int PathFileExistsW(PointerBase pszPath);

static final class Directives implements CContext.Directives {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ public void changeDir() throws IOException {
assertEquals(tmpDirAbs, curDir);
}

@Test
public void changeDirUnicode() throws IOException {
var tmpDir = TMP_DIR.newFolder().toPath();
var subDir = tmpDir.resolve("使用者");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test!

var dirCreated = subDir.toFile().mkdir();
assertTrue(dirCreated);
var subDirAbs = subDir.toAbsolutePath().toRealPath().toString();
var succeeded = nativeApi.changeWorkingDir(subDirAbs);
assertTrue(succeeded);
var curDir = nativeApi.currentWorkingDir();
assertEquals(subDirAbs, curDir);
}

@Test
public void changeDir_NonExistingDir() throws IOException {
var tmpDir = TMP_DIR.newFolder().toPath();
Expand Down
Loading