Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the default location for Enso projects #10318

Merged
merged 18 commits into from
Jun 22, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @file This module contains the logic for the detection of user-specific desktop environment attributes.
*/

import * as childProcess from 'node:child_process'
import * as os from 'node:os'
import * as path from 'node:path'

export const DOCUMENTS = getDocumentsPath()

const CHILD_PROCESS_TIMEOUT = 3000

/**
* Detects path of the user documents directory depending on the operating system.
*/
function getDocumentsPath(): string | undefined {
if (process.platform === 'linux') {
return getLinuxDocumentsPath()
} else if (process.platform === 'darwin') {
return getMacOsDocumentsPath()
} else if (process.platform === 'win32') {
return getWindowsDocumentsPath()
} else {
return
}
}

/**
* Returns the user documents path on Linux.
*/
function getLinuxDocumentsPath(): string {
const xdgDocumentsPath = getXdgDocumentsPath()

return xdgDocumentsPath ?? path.join(os.homedir(), 'enso')
}

/**
* Gets the documents directory from the XDG directory management system.
*/
function getXdgDocumentsPath(): string | undefined {
const out = childProcess.spawnSync('xdg-user-dir', ['DOCUMENTS'], {
timeout: CHILD_PROCESS_TIMEOUT,
})

if (out.error !== undefined) {
return
} else {
return out.stdout.toString().trim()
}
}

/**
* Get the user documents path. On macOS, `Documents` acts as a symlink pointing to the
* real locale-specific user documents directory.
*/
function getMacOsDocumentsPath(): string {
return path.join(os.homedir(), 'Documents')
}

/**
* Get the path to the `My Documents` Windows directory.
*/
function getWindowsDocumentsPath(): string | undefined {
const out = childProcess.spawnSync(
'reg',
[
'query',
'"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ShellFolders"',
'/v',
'personal',
],
{ timeout: CHILD_PROCESS_TIMEOUT }
)

if (out.error !== undefined) {
return
} else {
const stdoutString = out.stdout.toString()
return stdoutString.split('\\s\\s+')[4]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as tar from 'tar'

import * as common from 'enso-common'
import * as buildUtils from 'enso-common/src/buildUtils'
import * as desktopEnvironment from './desktopEnvironment'

const logger = console

Expand Down Expand Up @@ -369,7 +370,12 @@ export function getProjectRoot(subtreePath: string): string | null {

/** Get the directory that stores Enso projects. */
export function getProjectsDirectory(): string {
return pathModule.join(os.homedir(), 'enso', 'projects')
const documentsPath = desktopEnvironment.DOCUMENTS
if (documentsPath === undefined) {
return pathModule.join(os.homedir(), 'enso', 'projects')
} else {
return pathModule.join(documentsPath, 'enso-projects')
}
}

/** Check if the given project is installed, i.e. can be opened with the Project Manager. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import * as fs from 'node:fs/promises'
import * as fsSync from 'node:fs'
import * as http from 'node:http'
import * as os from 'node:os'
import * as path from 'node:path'

import * as isHiddenFile from 'is-hidden-file'
Expand All @@ -22,7 +21,7 @@ import * as projectManagement from './projectManagement'
const HTTP_STATUS_OK = 200
const HTTP_STATUS_BAD_REQUEST = 400
const HTTP_STATUS_NOT_FOUND = 404
const PROJECTS_ROOT_DIRECTORY = path.join(os.homedir(), 'enso/projects')
const PROJECTS_ROOT_DIRECTORY = projectManagement.getProjectsDirectory()

// =============
// === Types ===
Expand Down
12 changes: 12 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
.value
)
.dependsOn(`akka-native`)
.dependsOn(`desktop-environment`)
.dependsOn(`version-output`)
.dependsOn(editions)
.dependsOn(`edition-updater`)
Expand Down Expand Up @@ -2791,6 +2792,17 @@ lazy val `benchmarks-common` =
)
.dependsOn(`polyglot-api`)

lazy val `desktop-environment` =
Copy link
Member

Choose a reason for hiding this comment

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

No 3rd party dependencies. I like such lightweight modules.

project
.in(file("lib/java/desktop-environment"))
.settings(
frgaalJavaCompilerSetting,
libraryDependencies ++= Seq(
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test
)
)

lazy val `bench-processor` = (project in file("lib/scala/bench-processor"))
.settings(
frgaalJavaCompilerSetting,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.enso.desktopenvironment;

import org.enso.desktopenvironment.directories.Directories;
import org.enso.desktopenvironment.directories.DirectoriesFactory;

public final class Platform {
Copy link
Member

Choose a reason for hiding this comment

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

enum Platform {
  LINUX, MAC, WINDOWS
}

might be better choice from an API perspective.


private static final String OS_NAME = "os.name";
private static final String LINUX = "linux";
private static final String MAC = "mac";
private static final String WINDOWS = "windows";

private Platform() {}

public static Directories getDirectories() {
return DirectoriesFactory.getInstance();
}

public static String getOsName() {
Copy link
Member

Choose a reason for hiding this comment

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

If you have to define the getOsName() method at all...

return System.getProperty(OS_NAME);
}

public static boolean isLinux() {
return System.getProperty(OS_NAME).toLowerCase().contains(LINUX);
4e6 marked this conversation as resolved.
Show resolved Hide resolved
}

public static boolean isMacOs() {
return System.getProperty(OS_NAME).toLowerCase().contains(MAC);
}

public static boolean isWindows() {
return System.getProperty(OS_NAME).toLowerCase().contains(WINDOWS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.enso.desktopenvironment.directories;

import java.nio.file.Path;

public interface Directories {
Copy link
Member

Choose a reason for hiding this comment

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

Do you want other code outside of this module to implement this interface? If not, then prevent it:

  • (in old Java days) I'd recommend abstract class with package private constructor
  • now there is also an option to defined sealed interface

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good old Java days 🤠


default Path getUserHome() {
return Path.of(System.getProperty("user.home"));
}

Path getDocuments() throws DirectoriesException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.enso.desktopenvironment.directories;
4e6 marked this conversation as resolved.
Show resolved Hide resolved

import java.io.IOException;

/** Indicates an issue when accessing user directories. */
public final class DirectoriesException extends IOException {
4e6 marked this conversation as resolved.
Show resolved Hide resolved

public DirectoriesException(String message) {
super(message);
}

public DirectoriesException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.enso.desktopenvironment.directories;

import org.enso.desktopenvironment.Platform;

public final class DirectoriesFactory {
4e6 marked this conversation as resolved.
Show resolved Hide resolved

private static final Directories INSTANCE = initDirectories();

private static Directories initDirectories() {
if (Platform.isLinux()) {
return new LinuxDirectories();
}

if (Platform.isMacOs()) {
return new MacOsDirectories();
}

if (Platform.isWindows()) {
return new WindowsDirectories();
}

throw new UnsupportedOperationException("Unsupported OS '" + Platform.getOsName() + "'");
}

private DirectoriesFactory() {}

public static Directories getInstance() {
return INSTANCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.enso.desktopenvironment.directories;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

class LinuxDirectories implements Directories {

private static final String[] PROCESS_XDG_DOCUMENTS = new String[] {"xdg-user-dir", "DOCUMENTS"};

/**
* Get the user 'Documents' directory.
*
* <p>Tries to obtain the documents directory from the XDG directory management system if
* available and falls back to {@code $HOME/enso}.
*
* @return the path to the user documents directory.
*/
@Override
public Path getDocuments() {
try {
return getXdgDocuments();
} catch (IOException | InterruptedException e) {
return getUserHome().resolve("enso");
}
}

private Path getXdgDocuments() throws IOException, InterruptedException {
var process = new ProcessBuilder(PROCESS_XDG_DOCUMENTS).start();
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for using xdg-user-dir I am looking forward for Enso to put the projects into the right directory on my Linux box!

process.waitFor(3, TimeUnit.SECONDS);

var documentsString =
new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);

return Path.of(documentsString.trim());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.enso.desktopenvironment.directories;

import java.io.IOException;
import java.nio.file.Path;

class MacOsDirectories implements Directories {

private static final String DOCUMENTS = "Documents";

/**
* Get the user documents path.
*
* <p>On macOS, the 'Documents' directory acts like a symlink and points to the real
* locale-dependent user documents folder.
*
* @return the path to the user documents directory.
* @throws DirectoriesException when unable to resolve the real documents path.
*/
@Override
public Path getDocuments() throws DirectoriesException {
try {
return getUserHome().resolve(DOCUMENTS).toRealPath();
} catch (IOException e) {
throw new DirectoriesException("Failed to resolve real MacOs documents path", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.enso.desktopenvironment.directories;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

class WindowsDirectories implements Directories {

private static final String[] PROCESS_REG_QUERY =
new String[] {
"reg",
"query",
"\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ShellFolders\"",
"/v",
"personal"
};

/**
* Get the path to 'My Documents' user directory.
*
* <p>Method uses the registry query that may not work on Windows XP versions and below.
*
* @return the 'My Documents' user directory path.
* @throws DirectoriesException when fails to detect the user documents directory.
*/
@Override
public Path getDocuments() throws DirectoriesException {
try {
var process = new ProcessBuilder(PROCESS_REG_QUERY).start();
Copy link
Member

Choose a reason for hiding this comment

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

Avoiding a process execution would be nicer, but not even stack over flow has a suggestion how to do that nicely.

process.waitFor(3, TimeUnit.SECONDS);

var stdoutString =
new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
var stdoutParts = stdoutString.split("\\s\\s+");
if (stdoutParts.length < 5) {
throw new DirectoriesException(
"Invalid Windows registry query output: '" + stdoutString + "'");
}

return Path.of(stdoutParts[4].trim());
} catch (IOException e) {
throw new DirectoriesException("Failed to run Windows registry query", e);
} catch (InterruptedException e) {
throw new DirectoriesException("Windows registry query timeout", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.enso.desktopenvironment;

import org.junit.Assert;
import org.junit.Test;

public class PlatformTest {

@Test
public void getDirectories() {
Assert.assertNotNull(Platform.getDirectories());
}
}
Loading
Loading