8250950: Allow per-user and system wide configuration of a jpackaged app
Reviewed-by: almatvee
This commit is contained in:
parent
124ba45fb8
commit
c37c8e5d34
@ -71,6 +71,18 @@ void launchApp() {
|
|||||||
<< _T("lib/runtime"));
|
<< _T("lib/runtime"));
|
||||||
} else {
|
} else {
|
||||||
ownerPackage.initAppLauncher(appLauncher);
|
ownerPackage.initAppLauncher(appLauncher);
|
||||||
|
|
||||||
|
tstring homeDir;
|
||||||
|
JP_TRY;
|
||||||
|
homeDir = SysInfo::getEnvVariable("HOME");
|
||||||
|
JP_CATCH_ALL;
|
||||||
|
|
||||||
|
if (!homeDir.empty()) {
|
||||||
|
appLauncher.addCfgFileLookupDir(FileUtils::mkpath()
|
||||||
|
<< homeDir << ".local" << ownerPackage.name());
|
||||||
|
appLauncher.addCfgFileLookupDir(FileUtils::mkpath()
|
||||||
|
<< homeDir << "." + ownerPackage.name());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string _JPACKAGE_LAUNCHER = "_JPACKAGE_LAUNCHER";
|
const std::string _JPACKAGE_LAUNCHER = "_JPACKAGE_LAUNCHER";
|
||||||
|
@ -46,7 +46,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.SIGN_BUNDLE;
|
|||||||
|
|
||||||
public abstract class MacBaseInstallerBundler extends AbstractBundler {
|
public abstract class MacBaseInstallerBundler extends AbstractBundler {
|
||||||
|
|
||||||
public final BundlerParamInfo<Path> APP_IMAGE_TEMP_ROOT =
|
private final BundlerParamInfo<Path> APP_IMAGE_TEMP_ROOT =
|
||||||
new StandardBundlerParam<>(
|
new StandardBundlerParam<>(
|
||||||
"mac.app.imageRoot",
|
"mac.app.imageRoot",
|
||||||
Path.class,
|
Path.class,
|
||||||
@ -156,15 +156,25 @@ public abstract class MacBaseInstallerBundler extends AbstractBundler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected Path prepareAppBundle(Map<String, ? super Object> params)
|
protected Path prepareAppBundle(Map<String, ? super Object> params)
|
||||||
throws PackagerException {
|
throws PackagerException, IOException {
|
||||||
|
Path appDir;
|
||||||
|
Path appImageRoot = APP_IMAGE_TEMP_ROOT.fetchFrom(params);
|
||||||
Path predefinedImage =
|
Path predefinedImage =
|
||||||
StandardBundlerParam.getPredefinedAppImage(params);
|
StandardBundlerParam.getPredefinedAppImage(params);
|
||||||
if (predefinedImage != null) {
|
if (predefinedImage != null) {
|
||||||
return predefinedImage;
|
appDir = appImageRoot.resolve(APP_NAME.fetchFrom(params) + ".app");
|
||||||
|
IOUtils.copyRecursive(predefinedImage, appDir);
|
||||||
|
} else {
|
||||||
|
appDir = appImageBundler.execute(params, appImageRoot);
|
||||||
}
|
}
|
||||||
Path appImageRoot = APP_IMAGE_TEMP_ROOT.fetchFrom(params);
|
|
||||||
|
|
||||||
return appImageBundler.execute(params, appImageRoot);
|
if (!StandardBundlerParam.isRuntimeInstaller(params)) {
|
||||||
|
new PackageFile(APP_NAME.fetchFrom(params)).save(
|
||||||
|
ApplicationLayout.macAppImage().resolveAt(appDir));
|
||||||
|
Files.deleteIfExists(AppImageFile.getPathInAppImage(appDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
return appDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -277,11 +277,8 @@ public class MacDmgBundler extends MacBaseInstallerBundler {
|
|||||||
Path finalDMG = outdir.resolve(MAC_INSTALLER_NAME.fetchFrom(params)
|
Path finalDMG = outdir.resolve(MAC_INSTALLER_NAME.fetchFrom(params)
|
||||||
+ INSTALLER_SUFFIX.fetchFrom(params) + ".dmg");
|
+ INSTALLER_SUFFIX.fetchFrom(params) + ".dmg");
|
||||||
|
|
||||||
Path srcFolder = APP_IMAGE_TEMP_ROOT.fetchFrom(params);
|
Path srcFolder = appLocation.getParent();
|
||||||
Path predefinedImage = StandardBundlerParam.getPredefinedAppImage(params);
|
if (StandardBundlerParam.isRuntimeInstaller(params)) {
|
||||||
if (predefinedImage != null) {
|
|
||||||
srcFolder = predefinedImage;
|
|
||||||
} else if (StandardBundlerParam.isRuntimeInstaller(params)) {
|
|
||||||
Path newRoot = Files.createTempDirectory(TEMP_ROOT.fetchFrom(params),
|
Path newRoot = Files.createTempDirectory(TEMP_ROOT.fetchFrom(params),
|
||||||
"root-");
|
"root-");
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -26,8 +26,10 @@
|
|||||||
#include "AppLauncher.h"
|
#include "AppLauncher.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "FileUtils.h"
|
#include "FileUtils.h"
|
||||||
|
#include "PackageFile.h"
|
||||||
#include "UnixSysInfo.h"
|
#include "UnixSysInfo.h"
|
||||||
#include "JvmLauncher.h"
|
#include "JvmLauncher.h"
|
||||||
|
#include "ErrorHandling.h"
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -49,17 +51,36 @@ void initJvmLauncher() {
|
|||||||
const tstring appImageRoot = FileUtils::dirname(FileUtils::dirname(
|
const tstring appImageRoot = FileUtils::dirname(FileUtils::dirname(
|
||||||
FileUtils::dirname(launcherPath)));
|
FileUtils::dirname(launcherPath)));
|
||||||
|
|
||||||
|
const tstring appDirPath = FileUtils::mkpath() << appImageRoot
|
||||||
|
<< _T("Contents/app");
|
||||||
|
|
||||||
|
const PackageFile pkgFile = PackageFile::loadFromAppDir(appDirPath);
|
||||||
|
|
||||||
// Create JVM launcher and save in global variable.
|
// Create JVM launcher and save in global variable.
|
||||||
jvmLauncher = AppLauncher()
|
AppLauncher appLauncher = AppLauncher()
|
||||||
.setImageRoot(appImageRoot)
|
.setImageRoot(appImageRoot)
|
||||||
.addJvmLibName(_T("Contents/Home/lib/libjli.dylib"))
|
.addJvmLibName(_T("Contents/Home/lib/libjli.dylib"))
|
||||||
// add backup - older version such as JDK11 have it in jli sub-dir
|
// add backup - older version such as JDK11 have it in jli sub-dir
|
||||||
.addJvmLibName(_T("Contents/Home/lib/jli/libjli.dylib"))
|
.addJvmLibName(_T("Contents/Home/lib/jli/libjli.dylib"))
|
||||||
.setAppDir(FileUtils::mkpath() << appImageRoot << _T("Contents/app"))
|
.setAppDir(appDirPath)
|
||||||
.setLibEnvVariableName(_T("DYLD_LIBRARY_PATH"))
|
.setLibEnvVariableName(_T("DYLD_LIBRARY_PATH"))
|
||||||
.setDefaultRuntimePath(FileUtils::mkpath() << appImageRoot
|
.setDefaultRuntimePath(FileUtils::mkpath() << appImageRoot
|
||||||
<< _T("Contents/runtime"))
|
<< _T("Contents/runtime"));
|
||||||
.createJvmLauncher();
|
|
||||||
|
if (!pkgFile.getPackageName().empty()) {
|
||||||
|
tstring homeDir;
|
||||||
|
JP_TRY;
|
||||||
|
homeDir = SysInfo::getEnvVariable("HOME");
|
||||||
|
JP_CATCH_ALL;
|
||||||
|
|
||||||
|
if (!homeDir.empty()) {
|
||||||
|
appLauncher.addCfgFileLookupDir(FileUtils::mkpath()
|
||||||
|
<< homeDir << "Library/Application Support"
|
||||||
|
<< pkgFile.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmLauncher = appLauncher.createJvmLauncher();
|
||||||
|
|
||||||
// Kick start JVM launching. The function wouldn't return!
|
// Kick start JVM launching. The function wouldn't return!
|
||||||
launchJvm();
|
launchJvm();
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
|
||||||
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
|
*
|
||||||
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License version 2 only, as
|
||||||
|
* published by the Free Software Foundation. Oracle designates this
|
||||||
|
* particular file as subject to the "Classpath" exception as provided
|
||||||
|
* by Oracle in the LICENSE file that accompanied this code.
|
||||||
|
*
|
||||||
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* version 2 for more details (a copy is included in the LICENSE file that
|
||||||
|
* accompanied this code).
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License version
|
||||||
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||||
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*
|
||||||
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||||
|
* or visit www.oracle.com if you need additional information or have any
|
||||||
|
* questions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package jdk.jpackage.internal;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class PackageFile {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns path to package file.
|
||||||
|
* @param appImageDir - path to application image
|
||||||
|
*/
|
||||||
|
public static Path getPathInAppImage(Path appImageDir) {
|
||||||
|
return ApplicationLayout.platformAppImage()
|
||||||
|
.resolveAt(appImageDir)
|
||||||
|
.appDirectory()
|
||||||
|
.resolve(FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
PackageFile(String packageName) {
|
||||||
|
Objects.requireNonNull(packageName);
|
||||||
|
this.packageName = packageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void save(ApplicationLayout appLayout) throws IOException {
|
||||||
|
Path dst = Optional.ofNullable(appLayout.appDirectory()).map(appDir -> {
|
||||||
|
return appDir.resolve(FILENAME);
|
||||||
|
}).orElse(null);
|
||||||
|
|
||||||
|
if (dst != null) {
|
||||||
|
Files.createDirectories(dst.getParent());
|
||||||
|
Files.writeString(dst, packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String packageName;
|
||||||
|
|
||||||
|
private final static String FILENAME = ".package";
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -110,9 +110,7 @@ bool AppLauncher::libEnvVariableContainsAppDir() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Jvm* AppLauncher::createJvmLauncher() const {
|
Jvm* AppLauncher::createJvmLauncher() const {
|
||||||
const tstring cfgFilePath = FileUtils::mkpath()
|
const tstring cfgFilePath = getCfgFilePath();
|
||||||
<< appDirPath << FileUtils::stripExeSuffix(
|
|
||||||
FileUtils::basename(launcherPath)) + _T(".cfg");
|
|
||||||
|
|
||||||
LOG_TRACE(tstrings::any() << "Launcher config file path: \""
|
LOG_TRACE(tstrings::any() << "Launcher config file path: \""
|
||||||
<< cfgFilePath << "\"");
|
<< cfgFilePath << "\"");
|
||||||
@ -160,3 +158,20 @@ Jvm* AppLauncher::createJvmLauncher() const {
|
|||||||
void AppLauncher::launch() const {
|
void AppLauncher::launch() const {
|
||||||
std::unique_ptr<Jvm>(createJvmLauncher())->launch();
|
std::unique_ptr<Jvm>(createJvmLauncher())->launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tstring AppLauncher::getCfgFilePath() const {
|
||||||
|
tstring_array::const_iterator it = cfgFileLookupDirs.begin();
|
||||||
|
tstring_array::const_iterator end = cfgFileLookupDirs.end();
|
||||||
|
const tstring cfgFileName = FileUtils::stripExeSuffix(
|
||||||
|
FileUtils::basename(launcherPath)) + _T(".cfg");
|
||||||
|
for (; it != end; ++it) {
|
||||||
|
const tstring cfgFilePath = FileUtils::mkpath() << *it << cfgFileName;
|
||||||
|
LOG_TRACE(tstrings::any() << "Check [" << cfgFilePath << "] file exit");
|
||||||
|
if (FileUtils::isFileExists(cfgFilePath)) {
|
||||||
|
return cfgFilePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileUtils::mkpath() << appDirPath << cfgFileName;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -45,6 +45,11 @@ public:
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppLauncher& addCfgFileLookupDir(const tstring& v) {
|
||||||
|
cfgFileLookupDirs.push_back(v);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
AppLauncher& setAppDir(const tstring& v) {
|
AppLauncher& setAppDir(const tstring& v) {
|
||||||
appDirPath = v;
|
appDirPath = v;
|
||||||
return *this;
|
return *this;
|
||||||
@ -71,6 +76,9 @@ public:
|
|||||||
|
|
||||||
void launch() const;
|
void launch() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
tstring getCfgFilePath() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
tstring_array args;
|
tstring_array args;
|
||||||
tstring launcherPath;
|
tstring launcherPath;
|
||||||
@ -79,6 +87,7 @@ private:
|
|||||||
tstring libEnvVarName;
|
tstring libEnvVarName;
|
||||||
tstring imageRoot;
|
tstring imageRoot;
|
||||||
tstring_array jvmLibNames;
|
tstring_array jvmLibNames;
|
||||||
|
tstring_array cfgFileLookupDirs;
|
||||||
bool initJvmFromCmdlineOnly;
|
bool initJvmFromCmdlineOnly;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
61
src/jdk.jpackage/share/native/applauncher/PackageFile.cpp
Normal file
61
src/jdk.jpackage/share/native/applauncher/PackageFile.cpp
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
|
||||||
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
|
*
|
||||||
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License version 2 only, as
|
||||||
|
* published by the Free Software Foundation. Oracle designates this
|
||||||
|
* particular file as subject to the "Classpath" exception as provided
|
||||||
|
* by Oracle in the LICENSE file that accompanied this code.
|
||||||
|
*
|
||||||
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* version 2 for more details (a copy is included in the LICENSE file that
|
||||||
|
* accompanied this code).
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License version
|
||||||
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||||
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*
|
||||||
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||||
|
* or visit www.oracle.com if you need additional information or have any
|
||||||
|
* questions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "kludge_c++11.h"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include "PackageFile.h"
|
||||||
|
#include "Log.h"
|
||||||
|
#include "FileUtils.h"
|
||||||
|
#include "ErrorHandling.h"
|
||||||
|
|
||||||
|
|
||||||
|
PackageFile::PackageFile(const tstring& v): packageName(v) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PackageFile PackageFile::loadFromAppDir(const tstring& appDirPath) {
|
||||||
|
tstring packageName;
|
||||||
|
const tstring packageFilePath =
|
||||||
|
FileUtils::mkpath() << appDirPath << _T(".package");
|
||||||
|
if (FileUtils::isFileExists(packageFilePath)) {
|
||||||
|
LOG_TRACE(tstrings::any() << "Read \"" << packageFilePath
|
||||||
|
<< "\" package file");
|
||||||
|
std::ifstream input(packageFilePath);
|
||||||
|
if (!input.good()) {
|
||||||
|
JP_THROW(tstrings::any() << "Error opening \"" << packageFilePath
|
||||||
|
<< "\" file: " << lastCRTError());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string utf8line;
|
||||||
|
if (std::getline(input, utf8line)) {
|
||||||
|
LOG_TRACE(tstrings::any()
|
||||||
|
<< "Package name is [" << utf8line << "]");
|
||||||
|
packageName = tstrings::any(utf8line).tstr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PackageFile(packageName);
|
||||||
|
}
|
48
src/jdk.jpackage/share/native/applauncher/PackageFile.h
Normal file
48
src/jdk.jpackage/share/native/applauncher/PackageFile.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
|
||||||
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
|
*
|
||||||
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License version 2 only, as
|
||||||
|
* published by the Free Software Foundation. Oracle designates this
|
||||||
|
* particular file as subject to the "Classpath" exception as provided
|
||||||
|
* by Oracle in the LICENSE file that accompanied this code.
|
||||||
|
*
|
||||||
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* version 2 for more details (a copy is included in the LICENSE file that
|
||||||
|
* accompanied this code).
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License version
|
||||||
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||||
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*
|
||||||
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||||
|
* or visit www.oracle.com if you need additional information or have any
|
||||||
|
* questions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef PackageFile_h
|
||||||
|
#define PackageFile_h
|
||||||
|
|
||||||
|
#include "tstrings.h"
|
||||||
|
|
||||||
|
|
||||||
|
class PackageFile {
|
||||||
|
public:
|
||||||
|
static PackageFile loadFromAppDir(const tstring& appDirPath);
|
||||||
|
|
||||||
|
tstring getPackageName() const {
|
||||||
|
return packageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
PackageFile(const tstring& packageName);
|
||||||
|
|
||||||
|
private:
|
||||||
|
tstring packageName;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PackageFile_h
|
@ -382,10 +382,12 @@ public class WinMsiBundler extends AbstractBundler {
|
|||||||
.runtimeDirectory()
|
.runtimeDirectory()
|
||||||
.resolve(Path.of("bin", "java.exe"));
|
.resolve(Path.of("bin", "java.exe"));
|
||||||
} else {
|
} else {
|
||||||
installerIcon = ApplicationLayout.windowsAppImage()
|
var appLayout = ApplicationLayout.windowsAppImage().resolveAt(appDir);
|
||||||
.resolveAt(appDir)
|
|
||||||
.launchersDirectory()
|
installerIcon = appLayout.launchersDirectory()
|
||||||
.resolve(appName + ".exe");
|
.resolve(appName + ".exe");
|
||||||
|
|
||||||
|
new PackageFile(appName).save(appLayout);
|
||||||
}
|
}
|
||||||
installerIcon = installerIcon.toAbsolutePath();
|
installerIcon = installerIcon.toAbsolutePath();
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
#include "WinApp.h"
|
#include "WinApp.h"
|
||||||
#include "Toolbox.h"
|
#include "Toolbox.h"
|
||||||
#include "FileUtils.h"
|
#include "FileUtils.h"
|
||||||
|
#include "PackageFile.h"
|
||||||
#include "UniqueHandle.h"
|
#include "UniqueHandle.h"
|
||||||
#include "ErrorHandling.h"
|
#include "ErrorHandling.h"
|
||||||
#include "WinSysInfo.h"
|
#include "WinSysInfo.h"
|
||||||
@ -133,6 +134,22 @@ tstring getJvmLibPath(const Jvm& jvm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void addCfgFileLookupDirForEnvVariable(
|
||||||
|
const PackageFile& pkgFile, AppLauncher& appLauncher,
|
||||||
|
const tstring& envVarName) {
|
||||||
|
|
||||||
|
tstring path;
|
||||||
|
JP_TRY;
|
||||||
|
path = SysInfo::getEnvVariable(envVarName);
|
||||||
|
JP_CATCH_ALL;
|
||||||
|
|
||||||
|
if (!path.empty()) {
|
||||||
|
appLauncher.addCfgFileLookupDir(FileUtils::mkpath() << path
|
||||||
|
<< pkgFile.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void launchApp() {
|
void launchApp() {
|
||||||
// [RT-31061] otherwise UI can be left in back of other windows.
|
// [RT-31061] otherwise UI can be left in back of other windows.
|
||||||
::AllowSetForegroundWindow(ASFW_ANY);
|
::AllowSetForegroundWindow(ASFW_ANY);
|
||||||
@ -141,13 +158,20 @@ void launchApp() {
|
|||||||
const tstring appImageRoot = FileUtils::dirname(launcherPath);
|
const tstring appImageRoot = FileUtils::dirname(launcherPath);
|
||||||
const tstring appDirPath = FileUtils::mkpath() << appImageRoot << _T("app");
|
const tstring appDirPath = FileUtils::mkpath() << appImageRoot << _T("app");
|
||||||
|
|
||||||
const AppLauncher appLauncher = AppLauncher().setImageRoot(appImageRoot)
|
const PackageFile pkgFile = PackageFile::loadFromAppDir(appDirPath);
|
||||||
|
|
||||||
|
AppLauncher appLauncher = AppLauncher().setImageRoot(appImageRoot)
|
||||||
.addJvmLibName(_T("bin\\jli.dll"))
|
.addJvmLibName(_T("bin\\jli.dll"))
|
||||||
.setAppDir(appDirPath)
|
.setAppDir(appDirPath)
|
||||||
.setLibEnvVariableName(_T("PATH"))
|
.setLibEnvVariableName(_T("PATH"))
|
||||||
.setDefaultRuntimePath(FileUtils::mkpath() << appImageRoot
|
.setDefaultRuntimePath(FileUtils::mkpath() << appImageRoot
|
||||||
<< _T("runtime"));
|
<< _T("runtime"));
|
||||||
|
|
||||||
|
if (!pkgFile.getPackageName().empty()) {
|
||||||
|
addCfgFileLookupDirForEnvVariable(pkgFile, appLauncher, _T("LOCALAPPDATA"));
|
||||||
|
addCfgFileLookupDirForEnvVariable(pkgFile, appLauncher, _T("APPDATA"));
|
||||||
|
}
|
||||||
|
|
||||||
const bool restart = !appLauncher.libEnvVariableContainsAppDir();
|
const bool restart = !appLauncher.libEnvVariableContainsAppDir();
|
||||||
|
|
||||||
std::unique_ptr<Jvm> jvm(appLauncher.createJvmLauncher());
|
std::unique_ptr<Jvm> jvm(appLauncher.createJvmLauncher());
|
||||||
|
@ -47,6 +47,7 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import jdk.jpackage.internal.AppImageFile;
|
import jdk.jpackage.internal.AppImageFile;
|
||||||
import jdk.jpackage.internal.ApplicationLayout;
|
import jdk.jpackage.internal.ApplicationLayout;
|
||||||
|
import jdk.jpackage.internal.PackageFile;
|
||||||
import static jdk.jpackage.test.AdditionalLauncher.forEachAdditionalLauncher;
|
import static jdk.jpackage.test.AdditionalLauncher.forEachAdditionalLauncher;
|
||||||
import jdk.jpackage.test.Functional.ThrowingConsumer;
|
import jdk.jpackage.test.Functional.ThrowingConsumer;
|
||||||
import jdk.jpackage.test.Functional.ThrowingFunction;
|
import jdk.jpackage.test.Functional.ThrowingFunction;
|
||||||
@ -761,38 +762,8 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
JPackageCommand assertAppLayout() {
|
JPackageCommand assertAppLayout() {
|
||||||
if (isPackageUnpacked() || isImagePackageType()) {
|
assertAppImageFile();
|
||||||
final Path rootDir = isPackageUnpacked() ? pathToUnpackedPackageFile(
|
assertPackageFile();
|
||||||
appInstallationDirectory()) : outputBundle();
|
|
||||||
final Path appImageFileName = AppImageFile.getPathInAppImage(
|
|
||||||
Path.of("")).getFileName();
|
|
||||||
try (Stream<Path> walk = ThrowingSupplier.toSupplier(
|
|
||||||
() -> Files.walk(rootDir)).get()) {
|
|
||||||
List<String> appImageFiles = walk
|
|
||||||
.filter(path -> path.getFileName().equals(appImageFileName))
|
|
||||||
.map(Path::toString)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
if (isImagePackageType() || (TKit.isOSX() && !isRuntime())) {
|
|
||||||
List<String> expected = List.of(
|
|
||||||
AppImageFile.getPathInAppImage(rootDir).toString());
|
|
||||||
TKit.assertStringListEquals(expected, appImageFiles,
|
|
||||||
String.format(
|
|
||||||
"Check there is only one file with [%s] name in the package",
|
|
||||||
appImageFileName));
|
|
||||||
} else {
|
|
||||||
TKit.assertStringListEquals(List.of(), appImageFiles,
|
|
||||||
String.format(
|
|
||||||
"Check there are no files with [%s] name in the package",
|
|
||||||
appImageFileName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (TKit.isOSX() && !isRuntime()) {
|
|
||||||
TKit.assertFileExists(AppImageFile.getPathInAppImage(
|
|
||||||
appInstallationDirectory()));
|
|
||||||
} else {
|
|
||||||
TKit.assertPathExists(AppImageFile.getPathInAppImage(
|
|
||||||
appInstallationDirectory()), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
TKit.assertDirectoryExists(appRuntimeDirectory());
|
TKit.assertDirectoryExists(appRuntimeDirectory());
|
||||||
|
|
||||||
@ -809,6 +780,54 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertAppImageFile() {
|
||||||
|
final Path lookupPath = AppImageFile.getPathInAppImage(Path.of(""));
|
||||||
|
|
||||||
|
if (isRuntime() || !isImagePackageType()) {
|
||||||
|
assertFileInAppImage(lookupPath, null);
|
||||||
|
} else {
|
||||||
|
assertFileInAppImage(lookupPath, lookupPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertPackageFile() {
|
||||||
|
final Path lookupPath = PackageFile.getPathInAppImage(Path.of(""));
|
||||||
|
|
||||||
|
if (isRuntime() || isImagePackageType() || TKit.isLinux()) {
|
||||||
|
assertFileInAppImage(lookupPath, null);
|
||||||
|
} else {
|
||||||
|
assertFileInAppImage(lookupPath, lookupPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertFileInAppImage(Path filename, Path expectedPath) {
|
||||||
|
if (filename.getNameCount() > 1) {
|
||||||
|
assertFileInAppImage(filename.getFileName(), expectedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Path rootDir = isImagePackageType() ? outputBundle() : pathToUnpackedPackageFile(
|
||||||
|
appInstallationDirectory());
|
||||||
|
|
||||||
|
try ( Stream<Path> walk = ThrowingSupplier.toSupplier(() -> Files.walk(
|
||||||
|
rootDir)).get()) {
|
||||||
|
List<String> files = walk.filter(path -> path.getFileName().equals(
|
||||||
|
filename)).map(Path::toString).toList();
|
||||||
|
|
||||||
|
if (expectedPath == null) {
|
||||||
|
TKit.assertStringListEquals(List.of(), files, String.format(
|
||||||
|
"Check there are no files with [%s] name in the package",
|
||||||
|
filename));
|
||||||
|
} else {
|
||||||
|
List<String> expected = List.of(
|
||||||
|
rootDir.resolve(expectedPath).toString());
|
||||||
|
TKit.assertStringListEquals(expected, files, String.format(
|
||||||
|
"Check there is only one file with [%s] name in the package",
|
||||||
|
filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JPackageCommand setUnpackedPackageLocation(Path path) {
|
JPackageCommand setUnpackedPackageLocation(Path path) {
|
||||||
verifyIsOfType(PackageType.NATIVE);
|
verifyIsOfType(PackageType.NATIVE);
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
package jdk.jpackage.test;
|
package jdk.jpackage.test;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -46,6 +47,7 @@ import java.util.Date;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -769,6 +771,34 @@ final public class TKit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a directory by creating all nonexistent parent directories first
|
||||||
|
* just like java.nio.file.Files#createDirectories() and returns
|
||||||
|
* java.io.Closeable that will delete all created nonexistent parent
|
||||||
|
* directories.
|
||||||
|
*/
|
||||||
|
public static Closeable createDirectories(Path dir) throws IOException {
|
||||||
|
Objects.requireNonNull(dir);
|
||||||
|
|
||||||
|
Collection<Path> dirsToDelete = new ArrayList<>();
|
||||||
|
|
||||||
|
Path curDir = dir;
|
||||||
|
while (!Files.exists(curDir)) {
|
||||||
|
dirsToDelete.add(curDir);
|
||||||
|
curDir = curDir.getParent();
|
||||||
|
}
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
|
||||||
|
return new Closeable() {
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
for (var dirToDelete : dirsToDelete) {
|
||||||
|
Files.deleteIfExists(dirToDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public final static class TextStreamVerifier {
|
public final static class TextStreamVerifier {
|
||||||
TextStreamVerifier(String value) {
|
TextStreamVerifier(String value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
@ -254,6 +254,14 @@ if [ -z "$run_all_tests" ]; then
|
|||||||
jtreg_args+=(-Djpackage.test.SQETest=yes)
|
jtreg_args+=(-Djpackage.test.SQETest=yes)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "$APPDATA" ]; then
|
||||||
|
# Looks like this is Windows.
|
||||||
|
# Explicitly add LOCALAPPDATA and APPDATA environment variables to the list
|
||||||
|
# of environment variables jtreg will pass to tests as by default it will not.
|
||||||
|
# This is needed for PerUserCfgTest test.
|
||||||
|
jtreg_args+=("-e:LOCALAPPDATA,APPDATA")
|
||||||
|
fi
|
||||||
|
|
||||||
jtreg_args+=("$test_actions")
|
jtreg_args+=("$test_actions")
|
||||||
|
|
||||||
# Drop arguments separator
|
# Drop arguments separator
|
||||||
|
185
test/jdk/tools/jpackage/share/PerUserCfgTest.java
Normal file
185
test/jdk/tools/jpackage/share/PerUserCfgTest.java
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
|
||||||
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
|
*
|
||||||
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License version 2 only, as
|
||||||
|
* published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* version 2 for more details (a copy is included in the LICENSE file that
|
||||||
|
* accompanied this code).
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License version
|
||||||
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||||
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*
|
||||||
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||||
|
* or visit www.oracle.com if you need additional information or have any
|
||||||
|
* questions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import jdk.jpackage.test.AdditionalLauncher;
|
||||||
|
import jdk.jpackage.test.PackageTest;
|
||||||
|
import jdk.jpackage.test.Annotations.Test;
|
||||||
|
import jdk.jpackage.test.Functional.ThrowingConsumer;
|
||||||
|
import jdk.jpackage.test.HelloApp;
|
||||||
|
import jdk.jpackage.test.JPackageCommand;
|
||||||
|
import jdk.jpackage.test.LinuxHelper;
|
||||||
|
import jdk.jpackage.test.PackageType;
|
||||||
|
import jdk.jpackage.test.TKit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test per-user configuration of app launchers created by jpackage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @test
|
||||||
|
* @summary pre-user configuration of app launchers
|
||||||
|
* @library ../helpers
|
||||||
|
* @key jpackagePlatformPackage
|
||||||
|
* @requires jpackage.test.SQETest == null
|
||||||
|
* @build jdk.jpackage.test.*
|
||||||
|
* @compile PerUserCfgTest.java
|
||||||
|
* @modules jdk.jpackage/jdk.jpackage.internal
|
||||||
|
* @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
|
||||||
|
* --jpt-run=PerUserCfgTest
|
||||||
|
*/
|
||||||
|
public class PerUserCfgTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public static void test() throws IOException {
|
||||||
|
// Create a number of .cfg files with different startup args
|
||||||
|
JPackageCommand cfgCmd = JPackageCommand.helloAppImage().setFakeRuntime()
|
||||||
|
.setArgumentValue("--dest", TKit.createTempDirectory("cfg-files").toString());
|
||||||
|
|
||||||
|
addLauncher(cfgCmd, "a");
|
||||||
|
addLauncher(cfgCmd, "b");
|
||||||
|
|
||||||
|
cfgCmd.execute();
|
||||||
|
|
||||||
|
new PackageTest().configureHelloApp().addInstallVerifier(cmd -> {
|
||||||
|
if (cmd.isPackageUnpacked("Not running per-user configuration tests")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path launcherPath = cmd.appLauncherPath();
|
||||||
|
if (!cmd.canRunLauncher(String.format(
|
||||||
|
"Not running %s launcher and per-user configuration tests",
|
||||||
|
launcherPath))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final PackageType type = cmd.packageType();
|
||||||
|
if (PackageType.MAC.contains(type)) {
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("a"),
|
||||||
|
getUserHomeDir().resolve("Library/Application Support").resolve(
|
||||||
|
cmd.name()), theCmd -> {
|
||||||
|
runMainLauncher(cmd, "a");
|
||||||
|
});
|
||||||
|
} else if (PackageType.LINUX.contains(type)) {
|
||||||
|
final String pkgName = LinuxHelper.getPackageName(cmd);
|
||||||
|
final Path homeDir = getUserHomeDir();
|
||||||
|
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("a"),
|
||||||
|
homeDir.resolve(".local").resolve(pkgName), theCmd -> {
|
||||||
|
runMainLauncher(cmd, "a");
|
||||||
|
});
|
||||||
|
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("b"),
|
||||||
|
homeDir.resolve("." + pkgName), theCmd -> {
|
||||||
|
runMainLauncher(cmd, "b");
|
||||||
|
});
|
||||||
|
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("b"),
|
||||||
|
homeDir.resolve("." + pkgName), theCmd -> {
|
||||||
|
runMainLauncher(cmd, "b");
|
||||||
|
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("a"),
|
||||||
|
homeDir.resolve(".local").resolve(pkgName),
|
||||||
|
theCmd2 -> {
|
||||||
|
runMainLauncher(cmd, "a");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (PackageType.WINDOWS.contains(type)) {
|
||||||
|
final Path appData = getDirFromEnvVariable("APPDATA");
|
||||||
|
final Path localAppData = getDirFromEnvVariable("LOCALAPPDATA");
|
||||||
|
|
||||||
|
if (appData == null || localAppData == null) {
|
||||||
|
TKit.trace(String.format(
|
||||||
|
"Not running per-user configuration tests because some of the environment varibles are not set. "
|
||||||
|
+ "Run jtreg with -e:APPDATA,LOCALAPPDATA option to fix the problem"));
|
||||||
|
} else {
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("a"),
|
||||||
|
appData.resolve(cmd.name()), theCmd -> {
|
||||||
|
runMainLauncher(cmd, "a");
|
||||||
|
});
|
||||||
|
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("b"),
|
||||||
|
localAppData.resolve(cmd.name()), theCmd -> {
|
||||||
|
runMainLauncher(cmd, "b");
|
||||||
|
});
|
||||||
|
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("b"),
|
||||||
|
appData.resolve(cmd.name()), theCmd -> {
|
||||||
|
runMainLauncher(cmd, "b");
|
||||||
|
|
||||||
|
withConfigFile(cmd, cfgCmd.appLauncherCfgPath("a"),
|
||||||
|
localAppData.resolve(cmd.name()),
|
||||||
|
theCmd2 -> {
|
||||||
|
runMainLauncher(cmd, "a");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runMainLauncher(cmd);
|
||||||
|
}).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addLauncher(JPackageCommand cmd, String name) {
|
||||||
|
new AdditionalLauncher(name) {
|
||||||
|
@Override
|
||||||
|
protected void verify(JPackageCommand cmd) {}
|
||||||
|
}.setDefaultArguments(name).applyTo(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path getUserHomeDir() {
|
||||||
|
return getDirFromEnvVariable("HOME");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path getDirFromEnvVariable(String envVariableName) {
|
||||||
|
return Optional.ofNullable(System.getenv(envVariableName)).map(Path::of).orElse(
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void withConfigFile(JPackageCommand cmd, Path srcCfgFile,
|
||||||
|
Path outputCfgFileDir, ThrowingConsumer<JPackageCommand> action) throws
|
||||||
|
Throwable {
|
||||||
|
Path targetCfgFile = outputCfgFileDir.resolve(cmd.appLauncherCfgPath(
|
||||||
|
null).getFileName());
|
||||||
|
TKit.assertPathExists(targetCfgFile, false);
|
||||||
|
try (var dirCleaner = TKit.createDirectories(targetCfgFile.getParent())) {
|
||||||
|
Files.copy(srcCfgFile, targetCfgFile);
|
||||||
|
try {
|
||||||
|
TKit.traceFileContents(targetCfgFile, "cfg file");
|
||||||
|
action.accept(cmd);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(targetCfgFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runMainLauncher(JPackageCommand cmd,
|
||||||
|
String... expectedArgs) {
|
||||||
|
|
||||||
|
HelloApp.assertApp(cmd.appLauncherPath()).addDefaultArguments(List.of(
|
||||||
|
expectedArgs)).executeAndVerifyOutput();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user