diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 1ff90146fe..4a527cd6f3 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -2212,29 +2212,54 @@ static public InputStream getLibStream(String filename) throws IOException { * something similar on Windows, a dot folder on Linux.) Removed this as a * preference for 3.0a3 because we need this to be stable, but adding back * for 4.0 beta 4 so that folks can do 'portable' versions again. + * + * @deprecated use processing.utils.Settings.getFolder() instead, this method will invoke AWT */ static public File getSettingsFolder() { - File settingsFolder = null; - - try { - settingsFolder = Platform.getSettingsFolder(); - - // create the folder if it doesn't exist already - if (!settingsFolder.exists()) { - if (!settingsFolder.mkdirs()) { - Messages.showError("Settings issues", - "Processing cannot run because it could not\n" + - "create a folder to store your settings at\n" + - settingsFolder, null); - } + var override = getSettingsOverride(); + if (override != null) { + return override; } - } catch (Exception e) { - Messages.showTrace("An rare and unknowable thing happened", - "Could not get the settings folder. Please report:\n" + - "http://github.com/processing/processing/issues/new", - e, true); - } - return settingsFolder; + try { + return processing.utils.Settings.getFolder(); + } catch (processing.utils.Settings.SettingsFolderException e) { + switch (e.getType()) { + case COULD_NOT_CREATE_FOLDER -> Messages.showError("Settings issues", + """ + Processing cannot run because it could not + create a folder to store your settings at + """ + e.getMessage(), null); + case WINDOWS_APPDATA_NOT_FOUND -> Messages.showError("Settings issues", + """ + Processing cannot run because it could not + find the AppData or LocalAppData folder on your system. + """, null); + case MACOS_LIBRARY_FOLDER_NOT_FOUND -> Messages.showError("Settings issues", + """ + Processing cannot run because it could not + find the Library folder on your system. + """, null); + case LINUX_CONFIG_FOLDER_NOT_FOUND -> Messages.showError("Settings issues", + """ + Processing cannot run because either your + XDG_CONFIG_HOME or SNAP_USER_COMMON is set + but the folder does not exist. + """, null); + case LINUX_SUDO_USER_ERROR -> Messages.showError("Settings issues", + """ + Processing cannot run because it was started + with sudo and Processing could not resolve + the original users home directory. + """, null); + default -> Messages.showTrace("An rare and unknowable thing happened", + """ + Could not get the settings folder. Please report: + http://github.com/processing/processing4/issues/new + """, + e, true); + } + } + throw new RuntimeException("Unreachable code in Base.getSettingsFolder()"); } diff --git a/app/src/processing/app/Platform.java b/app/src/processing/app/Platform.java index ed76318b98..3372e3f9bc 100644 --- a/app/src/processing/app/Platform.java +++ b/app/src/processing/app/Platform.java @@ -40,7 +40,7 @@ import java.util.Map; -public class Platform { +public class Platform extends processing.utils.Platform { static DefaultPlatform inst; /* @@ -136,12 +136,7 @@ static public float getSystemZoom() { } - static public File getSettingsFolder() throws Exception { - return inst.getSettingsFolder(); - } - - - static public File getDefaultSketchbookFolder() throws Exception { + static public File getDefaultSketchbookFolder() throws Exception { return inst.getDefaultSketchbookFolder(); } @@ -303,28 +298,7 @@ static public int getIndex(String platformName) { // the MACOSX constant would instead read as the LINUX constant. - /** - * returns true if Processing is running on a Mac OS X machine. - */ - static public boolean isMacOS() { - return System.getProperty("os.name").contains("Mac"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - /** - * returns true if running on windows. - */ - static public boolean isWindows() { - return System.getProperty("os.name").contains("Windows"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - - /** - * true if running on linux. - */ - static public boolean isLinux() { - return System.getProperty("os.name").contains("Linux"); //$NON-NLS-1$ //$NON-NLS-2$ - } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index c5645c9bbc..94f7d1250f 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -3,10 +3,13 @@ package processing.app import androidx.compose.runtime.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import processing.utils.Settings import java.io.File import java.io.InputStream -import java.nio.file.* -import java.util.Properties +import java.nio.file.FileSystems +import java.nio.file.StandardWatchEventKinds +import java.nio.file.WatchEvent +import java.util.* const val PREFERENCES_FILE_NAME = "preferences.txt" @@ -20,7 +23,7 @@ fun PlatformStart(){ fun loadPreferences(): Properties{ PlatformStart() - val settingsFolder = Platform.getSettingsFolder() + val settingsFolder = Settings.getFolder() val preferencesFile = settingsFolder.resolve(PREFERENCES_FILE_NAME) if(!preferencesFile.exists()){ diff --git a/app/src/processing/app/platform/DefaultPlatform.java b/app/src/processing/app/platform/DefaultPlatform.java index 18997755b7..54f0ec2788 100644 --- a/app/src/processing/app/platform/DefaultPlatform.java +++ b/app/src/processing/app/platform/DefaultPlatform.java @@ -23,24 +23,19 @@ package processing.app.platform; -import java.awt.Desktop; -import java.awt.Font; -import java.io.File; - -import javax.swing.UIManager; -import javax.swing.border.EmptyBorder; - import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.FlatLightLaf; -import com.sun.jna.Library; -import com.sun.jna.Native; - import processing.app.Base; import processing.app.Preferences; import processing.app.ui.Toolkit; import processing.awt.ShimAWT; import processing.core.PApplet; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.io.File; + /** * Used by Base for platform-specific tweaking, for instance finding the @@ -206,24 +201,7 @@ public void setInterfaceZoom() throws Exception { public void saveLanguage(String languageCode) { } - /** - * This function should throw an exception or return a value. - * Do not return null. - */ - public File getSettingsFolder() throws Exception { - File override = Base.getSettingsOverride(); - if (override != null) { - return override; - } - - // If no subclass has a behavior, default to making a - // ".processing" directory in the user's home directory. - File home = new File(System.getProperty("user.home")); - return new File(home, ".processing"); - } - - - /** + /** * @return if not overridden, a folder named "sketchbook" in user.home. * @throws Exception so that subclasses can throw a fit */ diff --git a/app/src/processing/app/platform/LinuxPlatform.java b/app/src/processing/app/platform/LinuxPlatform.java index 3426144cae..ddfb4f3c43 100644 --- a/app/src/processing/app/platform/LinuxPlatform.java +++ b/app/src/processing/app/platform/LinuxPlatform.java @@ -22,16 +22,13 @@ package processing.app.platform; -import java.io.File; -import java.awt.Desktop; -import java.awt.Toolkit; - import processing.app.Base; -import processing.app.Messages; import processing.app.Preferences; import processing.core.PApplet; import javax.swing.*; +import java.awt.*; +import java.io.File; public class LinuxPlatform extends DefaultPlatform { @@ -90,40 +87,7 @@ static public String getHomeDir(String user) throws Exception { } - @Override - public File getSettingsFolder() throws Exception { - File override = Base.getSettingsOverride(); - if (override != null) { - return override; - } - - // https://github.com/processing/processing4/issues/203 - // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - - File configHome = null; - - // Check to see if the user has set a different location for their config - String configHomeEnv = System.getenv("XDG_CONFIG_HOME"); - if (configHomeEnv != null && !configHomeEnv.isBlank()) { - configHome = new File(configHomeEnv); - if (!configHome.exists()) { - Messages.err("XDG_CONFIG_HOME is set to " + configHomeEnv + " but does not exist."); - configHome = null; // don't use non-existent folder - } - } - String snapUserCommon = System.getenv("SNAP_USER_COMMON"); - if (snapUserCommon != null && !snapUserCommon.isBlank()) { - configHome = new File(snapUserCommon); - } - // If not set properly, use the default - if (configHome == null) { - configHome = new File(getHomeDir(), ".config"); - } - return new File(configHome, "processing"); - } - - - @Override + @Override public File getDefaultSketchbookFolder() throws Exception { return new File(getHomeDir(), "sketchbook"); } diff --git a/app/src/processing/app/platform/MacPlatform.java b/app/src/processing/app/platform/MacPlatform.java index f26c8f2c66..59f016b17f 100644 --- a/app/src/processing/app/platform/MacPlatform.java +++ b/app/src/processing/app/platform/MacPlatform.java @@ -22,23 +22,20 @@ package processing.app.platform; +import processing.app.Base; +import processing.app.Messages; +import processing.app.ui.About; +import processing.core.PApplet; +import processing.data.StringList; + +import javax.swing.*; import java.awt.*; -import java.awt.desktop.AppReopenedEvent; import java.awt.desktop.AppReopenedListener; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; -import javax.swing.JMenu; -import javax.swing.JMenuBar; - -import processing.app.Base; -import processing.app.Messages; -import processing.app.ui.About; -import processing.core.PApplet; -import processing.data.StringList; - /** * Platform handler for macOS. @@ -112,16 +109,7 @@ public void initBase(Base base) { } - public File getSettingsFolder() throws Exception { - File override = Base.getSettingsOverride(); - if (override != null) { - return override; - } - return new File(getLibraryFolder(), "Processing"); - } - - - public File getDefaultSketchbookFolder() throws Exception { + public File getDefaultSketchbookFolder() throws Exception { return new File(getDocumentsFolder(), "Processing"); } @@ -144,19 +132,6 @@ public void openURL(String url) throws Exception { // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - // TODO I suspect this won't work much longer, since access to the user's - // home directory seems verboten on more recent macOS versions [fry 191008] - // However, anecdotally it seems that just using the name works, - // and the localization is handled transparently. [fry 220116] - // https://github.com/processing/processing4/issues/9 - protected String getLibraryFolder() throws FileNotFoundException { - File folder = new File(System.getProperty("user.home"), "Library"); - if (!folder.exists()) { - throw new FileNotFoundException("Folder missing: " + folder); - } - return folder.getAbsolutePath(); - } - // TODO See above, and https://github.com/processing/processing4/issues/9 protected String getDocumentsFolder() throws FileNotFoundException { diff --git a/app/src/processing/app/platform/WindowsPlatform.java b/app/src/processing/app/platform/WindowsPlatform.java index b74a1674c3..3ec8941a98 100644 --- a/app/src/processing/app/platform/WindowsPlatform.java +++ b/app/src/processing/app/platform/WindowsPlatform.java @@ -22,20 +22,22 @@ package processing.app.platform; -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; - import com.sun.jna.Library; import com.sun.jna.Native; -import com.sun.jna.platform.win32.*; - -import processing.app.*; +import com.sun.jna.platform.win32.GDI32; +import com.sun.jna.platform.win32.Shell32Util; +import com.sun.jna.platform.win32.ShlObj; +import com.sun.jna.platform.win32.WinDef; +import processing.app.Base; +import processing.app.Messages; +import processing.app.Preferences; import processing.app.platform.WindowsRegistry.REGISTRY_ROOT_KEY; - import processing.core.PApplet; +import java.awt.*; +import java.io.File; +import java.io.UnsupportedEncodingException; + // With the changes to include .pyde files for 3.4, this class is // a bit of a mess. Registering a single extension has moved to @@ -351,54 +353,6 @@ protected void checkPath() { } - // looking for Documents and Settings/blah/Application Data/Processing - public File getSettingsFolder() throws Exception { - File override = Base.getSettingsOverride(); - if (override != null) { - return override; - } - - try { - String appDataRoaming = getAppDataPath(); - if (appDataRoaming != null) { - File settingsFolder = new File(appDataRoaming, APP_NAME); - if (settingsFolder.exists() || settingsFolder.mkdirs()) { - return settingsFolder; - } - } - - String appDataLocal = getLocalAppDataPath(); - if (appDataLocal != null) { - File settingsFolder = new File(appDataLocal, APP_NAME); - if (settingsFolder.exists() || settingsFolder.mkdirs()) { - return settingsFolder; - } - } - - if (appDataRoaming == null && appDataLocal == null) { - throw new IOException("Could not get the AppData folder"); - } - - // https://github.com/processing/processing/issues/3838 - throw new IOException("Permissions error: make sure that " + - appDataRoaming + " or " + appDataLocal + - " is writable."); - - } catch (UnsatisfiedLinkError ule) { - String path = new File("lib").getCanonicalPath(); - - String msg = Util.containsNonASCII(path) ? - """ - Please move Processing to a location with only - ASCII characters in the path and try again. - https://github.com/processing/processing/issues/3543 - """ : - "Could not find JNA support files, please reinstall Processing."; - Messages.showError("Windows JNA Problem", msg, ule); - return null; // unreachable - } - } - /* What's happening internally with JNA https://github.com/java-native-access/jna/blob/master/contrib/platform/src/com/sun/jna/platform/win32/Shell32.java @@ -413,19 +367,7 @@ public File getSettingsFolder() throws Exception { */ - /** Get the Users\name\AppData\Roaming path to write settings files. */ - static private String getAppDataPath() { - return Shell32Util.getSpecialFolderPath(ShlObj.CSIDL_APPDATA, true); - } - - - /** Get the Users\name\AppData\Local path as a settings fallback. */ - static private String getLocalAppDataPath() { - return Shell32Util.getSpecialFolderPath(ShlObj.CSIDL_LOCAL_APPDATA, true); - } - - - /** Get the Documents and Settings\name\My Documents\Processing folder. */ + /** Get the Documents and Settings\name\My Documents\Processing folder. */ public File getDefaultSketchbookFolder() throws Exception { String documentsPath = getDocumentsPath(); if (documentsPath != null) { diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt index 254c0946c1..9165c6c1bd 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -3,11 +3,10 @@ package processing.app.ui.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf -import processing.app.LocalPreferences import processing.app.Messages -import processing.app.Platform import processing.app.PlatformStart import processing.app.watchFile +import processing.utils.Settings import java.io.File import java.io.InputStream import java.util.* @@ -34,7 +33,7 @@ val LocalLocale = compositionLocalOf { Locale() } fun LocaleProvider(content: @Composable () -> Unit) { PlatformStart() - val settingsFolder = Platform.getSettingsFolder() + val settingsFolder = Settings.getFolder() val languageFile = File(settingsFolder, "language.txt") watchFile(languageFile) diff --git a/app/utils/src/main/java/processing/utils/Platform.java b/app/utils/src/main/java/processing/utils/Platform.java new file mode 100644 index 0000000000..497613e51a --- /dev/null +++ b/app/utils/src/main/java/processing/utils/Platform.java @@ -0,0 +1,26 @@ +package processing.utils; + +public class Platform { + /** + * returns true if Processing is running on a Mac OS X machine. + */ + static public boolean isMacOS() { + return System.getProperty("os.name").contains("Mac"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + + /** + * returns true if running on windows. + */ + static public boolean isWindows() { + return System.getProperty("os.name").contains("Windows"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + + /** + * true if running on linux. + */ + static public boolean isLinux() { + return System.getProperty("os.name").contains("Linux"); //$NON-NLS-1$ //$NON-NLS-2$ + } +} diff --git a/app/utils/src/main/java/processing/utils/Settings.java b/app/utils/src/main/java/processing/utils/Settings.java new file mode 100644 index 0000000000..7ad34b9b76 --- /dev/null +++ b/app/utils/src/main/java/processing/utils/Settings.java @@ -0,0 +1,149 @@ +package processing.utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.util.Optional; + +public class Settings { + public static File getFolder() throws SettingsFolderException { + try { + var folder = getFolderForPlatform(); + if (!folder.exists() && !folder.mkdirs()) { + throw new SettingsFolderException(SettingsFolderException.Type.COULD_NOT_CREATE_FOLDER, folder.getAbsolutePath()); + } + return folder; + } catch (RuntimeException e) { + throw new SettingsFolderException(SettingsFolderException.Type.UNKNOWN); + } + } + + private static File getFolderForPlatform() throws SettingsFolderException { + var settingsOverride = System.getProperty("processing.settings.folder"); + if (settingsOverride != null && !settingsOverride.isEmpty()) { + return new File(settingsOverride); + } + + var portableSettings = FindPortableSettings(); + if (portableSettings.isPresent()) { + return portableSettings.get(); + } + + if (Platform.isWindows()) { + var options = new String[]{ + "APPDATA", + "LOCALAPPDATA" + }; + for (String option : options) { + var folder = new File(System.getenv(option), "Processing"); + if (!folder.exists() && !folder.mkdirs()) { + continue; + } + return folder; + } + throw new SettingsFolderException(SettingsFolderException.Type.WINDOWS_APPDATA_NOT_FOUND); + } + if (Platform.isMacOS()) { + var folder = new File(System.getProperty("user.home"), "Library"); + if (!folder.exists()) { + throw new SettingsFolderException(SettingsFolderException.Type.MACOS_LIBRARY_FOLDER_NOT_FOUND); + } + return new File(folder, "Processing"); + } + if (Platform.isLinux()) { + var options = new String[]{ + "SNAP_USER_COMMON", + "XDG_CONFIG_HOME" + }; + for (String option : options) { + var configHomeEnv = System.getenv(option); + if (configHomeEnv == null || configHomeEnv.isBlank()) { + continue; + } + var parentFolder = new File(configHomeEnv); + if (!parentFolder.exists()) { + throw new SettingsFolderException(SettingsFolderException.Type.LINUX_CONFIG_FOLDER_NOT_FOUND); + } + var folder = new File(parentFolder, "processing"); + if (!folder.exists() && !folder.mkdirs()) { + continue; + } + return folder; + } + var subfolder = "/.config/processing"; + var isSudo = System.getenv("SUDO_USER"); + if (isSudo == null || isSudo.isEmpty()) { + return new File(System.getProperty("user.home") + subfolder); + } + // If user is SUDO_USER, try to get their home directory + try { + var process = Runtime.getRuntime().exec( + new String[]{ + "/bin/sh", "-c", "echo ~" + isSudo + } + ); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + return new File(reader.readLine() + subfolder); + } + } catch (Exception e) { + throw new SettingsFolderException(SettingsFolderException.Type.LINUX_SUDO_USER_ERROR); + } + } + + // If all else fails, use ~/.processing + return new File(System.getProperty("user.home"), ".processing"); + } + + /** + * find a preferences.txt file in the same folder as the running jar/executable + * + * @return Optional File pointing to preferences.txt if found, empty otherwise + */ + private static Optional FindPortableSettings() { + var command = ProcessHandle.current().info().command(); + if (command.isEmpty()) return Optional.empty(); + + var path = command.get(); + path = path.replaceAll("/[^/]+$", ""); + + if (Platform.isMacOS()) { + // On macOS, the executable is inside the .app bundle, so we need to go up to above the .app folder + path = path.replaceAll("/[^/]+\\.app/.*$", ""); + } + var file = new File(path, "preferences.txt"); + if (System.getenv().containsKey("DEBUG")) + System.out.println("Looking for portable settings at: " + file.getAbsolutePath()); + + if (!file.exists()) { + return Optional.empty(); + } + return Optional.of(new File(path)); + + } + + public static class SettingsFolderException extends Exception { + public enum Type { + COULD_NOT_CREATE_FOLDER, + WINDOWS_APPDATA_NOT_FOUND, + MACOS_LIBRARY_FOLDER_NOT_FOUND, + LINUX_CONFIG_FOLDER_NOT_FOUND, + LINUX_SUDO_USER_ERROR, + UNKNOWN + } + + private final Type type; + + public SettingsFolderException(Type type) { + this.type = type; + } + + public SettingsFolderException(Type type, String message) { + super(message); + this.type = type; + } + + public Type getType() { + return type; + } + } +} diff --git a/app/utils/src/test/java/SettingsTest.java b/app/utils/src/test/java/SettingsTest.java new file mode 100644 index 0000000000..c03fb6732d --- /dev/null +++ b/app/utils/src/test/java/SettingsTest.java @@ -0,0 +1,40 @@ +import org.junit.jupiter.api.Test; +import processing.utils.Settings; + +import java.io.IOException; +import java.nio.file.Files; + +public class SettingsTest { + + /** + * Requesting the settings folder should create it if it doesn't exist + */ + @Test + public void testSettingsFolder() { + try { + var folder = Settings.getFolder(); + assert (folder.exists()); + } catch (Settings.SettingsFolderException e) { + assert (false); + } + } + + /** + * Overriding the settings folder via system property should work + */ + @Test + public void testOverrideFolder() throws IOException { + var settings = Files.createTempDirectory("settings_test"); + System.setProperty("processing.settings.folder", settings.toString()); + + try { + var folder = Settings.getFolder(); + assert (folder.toPath().toString().equals(settings.toString())); + } catch (Settings.SettingsFolderException e) { + assert (false); + } finally { + System.clearProperty("processing.settings.folder"); + Files.deleteIfExists(settings); + } + } +}