Browse Source

Move string::absolute, directory, filename, filenameBase, and filenameExtension to system::getAbsolute, getDirectory, getFilename, getStem, and getExtension. Reimplement most system:: functions using std::experimental::filesystem. Add system::doesExist, getFileSize, and getTempDir.

tags/v2.0.0
Andrew Belt 4 years ago
parent
commit
3fbd0f77a9
13 changed files with 431 additions and 529 deletions
  1. +0
    -6
      include/common.hpp
  2. +1
    -25
      include/string.hpp
  3. +87
    -34
      include/system.hpp
  4. +1
    -1
      src/app/MenuBar.cpp
  5. +20
    -28
      src/app/ModuleWidget.cpp
  6. +1
    -1
      src/app/Scene.cpp
  7. +0
    -7
      src/common.cpp
  8. +20
    -30
      src/patch.cpp
  9. +35
    -49
      src/plugin.cpp
  10. +0
    -49
      src/string.cpp
  11. +263
    -297
      src/system.cpp
  12. +2
    -1
      src/updater.cpp
  13. +1
    -1
      src/window.cpp

+ 0
- 6
include/common.hpp View File

@@ -205,18 +205,12 @@ static_assert(sizeof(wchar_t) == 2);

// Windows C standard functions are ASCII-8 instead of UTF-8, so redirect these functions to wrappers which convert to UTF-8
#define fopen fopen_u8
#define remove remove_u8
#define rename rename_u8

extern "C" {
FILE* fopen_u8(const char* filename, const char* mode);
int remove_u8(const char* path);
int rename_u8(const char* oldname, const char* newname);
}

namespace std {
using ::fopen_u8;
using ::remove_u8;
using ::rename_u8;
}
#endif

+ 1
- 25
include/string.hpp View File

@@ -28,31 +28,6 @@ std::string ellipsizePrefix(const std::string& s, size_t len);
bool startsWith(const std::string& str, const std::string& prefix);
bool endsWith(const std::string& str, const std::string& suffix);

/** Extracts the directory of the path.
Example: directory("dir/file.txt") // "dir"
Calls POSIX dirname().
*/
std::string directory(const std::string& path);
/** Extracts the filename of the path.
Example: directory("dir/file.txt") // "file.txt"
Calls POSIX basename().
*/
std::string filename(const std::string& path);
/** Extracts the portion of a filename without the extension.
Example: filenameBase("file.txt") // "file"
Note: Only works on filenames. Call filename(path) to get the filename of the path.
*/
std::string filenameBase(const std::string& filename);
/** Extracts the extension of a filename.
Example: filenameExtension("file.txt") // "txt"
Note: Only works on filenames. Call filename(path) to get the filename of the path.
*/
std::string filenameExtension(const std::string& filename);
/** Returns the canonicalized absolute path pointed to by `path`, following symlinks.
Returns "" if the symbol is not found.
*/
std::string absolutePath(const std::string& path);

/** Scores how well a query matches a string.
A score of 0 means no match.
The score is arbitrary and is only meaningful for sorting.
@@ -85,5 +60,6 @@ std::string U16toU8(const std::wstring& w);
std::wstring U8toU16(const std::string& s);
#endif


} // namespace string
} // namespace rack

+ 87
- 34
include/system.hpp View File

@@ -2,54 +2,117 @@
#include <list>

#include <common.hpp>
#include <experimental/filesystem>


namespace rack {


// In C++17, this will be `std::filesystem`
namespace filesystem = std::experimental::filesystem;


/** Cross-platform functions for operating systems routines
*/
namespace system {


/** Returns a list of all entries (directories, files, symbols) in a directory.
Sorted alphabetically.
// Filesystem

/** Returns a list of all entries (directories, files, symbolic links, etc) in a directory.
`depth` is the number of directories to recurse. 0 depth does not recurse. -1 depth recurses infinitely.
*/
std::list<std::string> getEntries(const std::string& path);
std::list<std::string> getEntriesRecursive(const std::string &path, int depth);
std::list<std::string> getEntries(const std::string& dirPath, int depth = 0);
bool doesExist(const std::string& path);
/** Returns whether the given path is a file. */
bool isFile(const std::string& path);
/** Returns whether the given path is a directory. */
bool isDirectory(const std::string& path);
/** Moves a file. */
void moveFile(const std::string& srcPath, const std::string& destPath);
/** Copies a file. */
void copyFile(const std::string& srcPath, const std::string& destPath);
uint64_t getFileSize(const std::string& path);
/** Moves a file or folder.
Does not overwrite the destination. If this behavior is needed, use remove() or removeRecursively() before moving.
*/
void rename(const std::string& srcPath, const std::string& destPath);
/** Copies a file or folder recursively. */
void copy(const std::string& srcPath, const std::string& destPath);
/** Creates a directory.
The parent directory must exist.
*/
void createDirectory(const std::string& path);
bool createDirectory(const std::string& path);
/** Creates all directories up to the path.
*/
void createDirectories(const std::string& path);
/** Deletes a directory.
The directory must be empty. Fails silently.
bool createDirectories(const std::string& path);
/** Deletes a file or empty directory.
Returns whether the deletion was successful.
*/
void removeDirectory(const std::string& path);
/** Deletes a directory if empty and all parent directories that are then empty.
bool remove(const std::string& path);
/** Deletes a file or directory recursively.
Returns the number of files and directories that were deleted.
*/
void removeDirectories(const std::string& path);
int removeRecursively(const std::string& path);
std::string getWorkingDirectory();
void setWorkingDirectory(const std::string& path);
std::string getTempDir();
std::string getAbsolute(const std::string& path);
/** Extracts the parent directory of the path.
Examples:
getDirectory("/var/tmp/example.txt") // "/var/tmp"
getDirectory("/") // ""
getDirectory("/var/tmp/.") // "/var/tmp"
*/
std::string getDirectory(const std::string& path);
/** Extracts the filename of the path.
Examples:
getFilename("/foo/bar.txt") // "bar.txt"
getFilename("/foo/.bar") // ".bar"
getFilename("/foo/bar/") // "."
getFilename("/foo/.") // "."
getFilename("/foo/..") // ".."
getFilename(".") // "."
getFilename("..") // ".."
getFilename("/") // "/"
*/
std::string getFilename(const std::string& path);
/** Extracts the portion of a filename without the extension.
Examples:
getExtension("/foo/bar.txt") // "bar"
getExtension("/foo/.bar") // ""
getExtension("/foo/foo.bar.baz.tar") // "foo.bar.baz"
*/
std::string getStem(const std::string& path);
/** Extracts the extension of a filename, including the dot.
Examples:
getExtension("/foo/bar.txt") // ".txt"
getExtension("/foo/bar.") // "."
getExtension("/foo/bar") // ""
getExtension("/foo/bar.txt/bar.cc") // ".cc"
getExtension("/foo/bar.txt/bar.") // "."
getExtension("/foo/bar.txt/bar") // ""
getExtension("/foo/.") // ""
getExtension("/foo/..") // ""
getExtension("/foo/.hidden") // ".hidden"
*/
std::string getExtension(const std::string& path);

/** Compresses the contents of a folder (recursively) to an archive.
Currently supports the "ustar zstd" format (.tar.zst)
An equivalent shell command is

tar -cf archivePath --zstd -C folderPath .
*/
void archiveFolder(const std::string& archivePath, const std::string& folderPath);
/** Extracts an archive into a folder.
An equivalent shell command is

tar -xf archivePath --zstd -C folderPath
*/
void unarchiveToFolder(const std::string& archivePath, const std::string& folderPath);


// Threading

/** Returns the number of logical simultaneous multithreading (SMT) (e.g. Intel Hyperthreaded) threads on the CPU. */
int getLogicalCoreCount();
/** Sets a name of the current thread for debuggers and OS-specific process viewers. */
void setThreadName(const std::string& name);

// Querying

/** Returns the caller's human-readable stack trace with "\n"-separated lines. */
std::string getStackTrace();
/** Returns the current number of nanoseconds since the epoch.
@@ -57,6 +120,10 @@ The goal of this function is to give the most precise (fine-grained) time availa
The epoch is undefined. Do not use this function to get absolute time, as it is different on each OS.
*/
int64_t getNanoseconds();
std::string getOperatingSystemInfo();

// Applications

/** Opens a URL, also happens to work with PDFs and folders.
Shell injection is possible, so make sure the URL is trusted or hard coded.
May block, so open in a new thread.
@@ -68,20 +135,6 @@ void openFolder(const std::string& path);
The launched process will continue running if the current process is closed.
*/
void runProcessDetached(const std::string& path);
std::string getOperatingSystemInfo();
/** Compresses the contents of a folder (recursively) to an archive.
Currently supports the "ustar zstd" format (.tar.zst)
An equivalent shell command is

tar -cf archivePath --zstd -C folderPath .
*/
void archiveFolder(const filesystem::path& archivePath, const filesystem::path& folderPath);
/** Extracts an archive into a folder.
An equivalent shell command is

tar -xf archivePath --zstd -C folderPath
*/
void unarchiveToFolder(const filesystem::path& archivePath, const filesystem::path& folderPath);


} // namespace system


+ 1
- 1
src/app/MenuBar.cpp View File

@@ -105,7 +105,7 @@ struct OpenRecentItem : ui::MenuItem {

for (const std::string& path : settings::recentPatchPaths) {
OpenPathItem* item = new OpenPathItem;
item->text = string::filename(path);
item->text = system::getFilename(path);
item->path = path;
menu->addChild(item);
}


+ 20
- 28
src/app/ModuleWidget.cpp View File

@@ -265,12 +265,11 @@ struct ModulePresetItem : ui::MenuItem {
bool hasPresets = false;
// Note: This is not cached, so opening this menu each time might have a bit of latency.
for (const std::string& presetPath : system::getEntries(presetDir)) {
std::string presetFilename = string::filename(presetPath);
if (string::filenameExtension(presetFilename) != "vcvm")
if (system::getExtension(presetPath) != ".vcvm")
continue;
hasPresets = true;

std::string presetName = string::filenameBase(presetFilename);
std::string presetName = system::getStem(presetPath);
// Remove "1_", "42_", "001_", etc at the beginning of preset filenames
std::regex r("^\\d*_");
presetName = std::regex_replace(presetName, r, "");
@@ -690,25 +689,22 @@ void ModuleWidget::loadDialog() {

// Delete directories if empty
DEFER({
system::removeDirectories(presetDir);
system::remove(presetDir);
system::remove(system::getDirectory(presetDir));
});

osdialog_filters* filters = osdialog_filters_parse(PRESET_FILTERS);
DEFER({
osdialog_filters_free(filters);
});
DEFER({osdialog_filters_free(filters);});

char* path = osdialog_file(OSDIALOG_OPEN, presetDir.c_str(), NULL, filters);
if (!path) {
char* pathC = osdialog_file(OSDIALOG_OPEN, presetDir.c_str(), NULL, filters);
if (!pathC) {
// No path selected
return;
}
DEFER({
free(path);
});
DEFER({free(pathC);});

try {
loadAction(path);
loadAction(pathC);
}
catch (Exception& e) {
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, e.what());
@@ -749,30 +745,26 @@ void ModuleWidget::saveDialog() {

// Delete directories if empty
DEFER({
system::removeDirectories(presetDir);
// These fail silently if the directories are not empty
system::remove(presetDir);
system::remove(system::getDirectory(presetDir));
});

osdialog_filters* filters = osdialog_filters_parse(PRESET_FILTERS);
DEFER({
osdialog_filters_free(filters);
});
DEFER({osdialog_filters_free(filters);});

char* path = osdialog_file(OSDIALOG_SAVE, presetDir.c_str(), "Untitled.vcvm", filters);
if (!path) {
char* pathC = osdialog_file(OSDIALOG_SAVE, presetDir.c_str(), "Untitled.vcvm", filters);
if (!pathC) {
// No path selected
return;
}
DEFER({
free(path);
});
DEFER({free(pathC);});

std::string pathStr = path;
std::string extension = string::filenameExtension(string::filename(pathStr));
if (extension == "") {
pathStr += ".vcvm";
}
std::string path = pathC;
if (system::getExtension(path) == "")
path += ".vcvm";

save(pathStr);
save(path);
}

template <class T, typename F>


+ 1
- 1
src/app/Scene.cpp View File

@@ -182,7 +182,7 @@ void Scene::onHoverKey(const event::HoverKey& e) {
void Scene::onPathDrop(const event::PathDrop& e) {
if (e.paths.size() >= 1) {
const std::string& path = e.paths[0];
if (string::filenameExtension(string::filename(path)) == "vcv") {
if (system::getExtension(path) == ".vcv") {
APP->patch->loadPathDialog(path);
e.consume(this);
return;


+ 0
- 7
src/common.cpp View File

@@ -31,12 +31,5 @@ FILE* fopen_u8(const char* filename, const char* mode) {
return _wfopen(rack::string::U8toU16(filename).c_str(), rack::string::U8toU16(mode).c_str());
}

int remove_u8(const char* path) {
return _wremove(rack::string::U8toU16(path).c_str());
}

int rename_u8(const char* oldname, const char* newname) {
return _wrename(rack::string::U8toU16(oldname).c_str(), rack::string::U8toU16(newname).c_str());
}

#endif

+ 20
- 30
src/patch.cpp View File

@@ -62,7 +62,7 @@ void PatchManager::save(std::string path) {
INFO("Saving patch %s", path.c_str());
saveAutosave();

system::archiveFolder(filesystem::u8path(path), asset::autosavePath);
system::archiveFolder(path, asset::autosavePath);
}


@@ -89,30 +89,26 @@ void PatchManager::saveAsDialog() {
std::string filename;
if (this->path == "") {
dir = asset::user("patches");
system::createDirectory(dir);
system::createDirectories(dir);
}
else {
dir = string::directory(this->path);
filename = string::filename(this->path);
dir = system::getDirectory(this->path);
filename = system::getFilename(this->path);
}

osdialog_filters* filters = osdialog_filters_parse(PATCH_FILTERS);
DEFER({
osdialog_filters_free(filters);
});
DEFER({osdialog_filters_free(filters);});

char* pathC = osdialog_file(OSDIALOG_SAVE, dir.c_str(), filename.c_str(), filters);
if (!pathC) {
// Fail silently
// Cancel silently
return;
}
DEFER({
std::free(pathC);
});
DEFER({std::free(pathC);});

// Append .vcv extension if no extension was given.
std::string path = pathC;
if (string::filenameExtension(string::filename(path)) == "") {
if (system::getExtension(path) == "") {
path += ".vcv";
}

@@ -150,9 +146,7 @@ void PatchManager::saveAutosave() {
json_t* rootJ = toJson();
if (!rootJ)
return;
DEFER({
json_decref(rootJ);
});
DEFER({json_decref(rootJ);});

// Write to temporary path and then rename it to the correct path
system::createDirectories(asset::autosavePath);
@@ -166,7 +160,8 @@ void PatchManager::saveAutosave() {

json_dumpf(rootJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9));
std::fclose(file);
system::moveFile(tmpPath, patchPath);
system::remove(patchPath);
system::rename(tmpPath, patchPath);
}


@@ -186,17 +181,16 @@ static bool isPatchLegacyPre2(std::string path) {
void PatchManager::load(std::string path) {
INFO("Loading patch %s", path.c_str());

filesystem::remove_all(asset::autosavePath);
filesystem::create_directories(asset::autosavePath);
system::removeRecursively(asset::autosavePath);
system::createDirectories(asset::autosavePath);

if (isPatchLegacyPre2(path)) {
// Move the .vcv file directly to "patch.json".
filesystem::path autosavePath = filesystem::u8path(asset::autosavePath);
filesystem::copy(filesystem::u8path(path), autosavePath / "patch.json");
// Copy the .vcv file directly to "patch.json".
system::copy(path, asset::autosavePath + "/patch.json");
}
else {
// Extract the .vcv file as a .tar.zst archive.
system::unarchiveToFolder(filesystem::u8path(path), asset::autosavePath);
system::unarchiveToFolder(path, asset::autosavePath);
}

loadAutosave();
@@ -241,7 +235,7 @@ void PatchManager::loadAutosave() {
FILE* file = std::fopen(patchPath.c_str(), "r");
if (!file) {
// Exit silently
// TODO Load template
// TODO Load template without causing infinite recursion
return;
}
DEFER({
@@ -255,9 +249,7 @@ void PatchManager::loadAutosave() {
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());
return;
}
DEFER({
json_decref(rootJ);
});
DEFER({json_decref(rootJ);});

fromJson(rootJ);
}
@@ -288,13 +280,11 @@ void PatchManager::loadDialog() {
system::createDirectory(dir);
}
else {
dir = string::directory(this->path);
dir = system::getDirectory(this->path);
}

osdialog_filters* filters = osdialog_filters_parse(PATCH_FILTERS);
DEFER({
osdialog_filters_free(filters);
});
DEFER({osdialog_filters_free(filters);});

char* pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, filters);
if (!pathC) {


+ 35
- 49
src/plugin.cpp View File

@@ -44,30 +44,27 @@ namespace plugin {
////////////////////

static void* loadLibrary(std::string libraryPath) {
#if defined ARCH_WIN
SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS);
std::wstring libraryFilenameW = string::U8toU16(libraryPath);
HINSTANCE handle = LoadLibraryW(libraryFilenameW.c_str());
SetErrorMode(0);
if (!handle) {
int error = GetLastError();
throw Exception(string::f("Failed to load library %s: code %d", libraryPath.c_str(), error));
}
#else
// Plugin uses -rpath=. so change working directory so it can find libRack.
std::string cwd = system::getWorkingDirectory();
system::setWorkingDirectory(asset::systemDir);
// And then change it back
DEFER({
system::setWorkingDirectory(cwd);
});
// Load library with dlopen
void* handle = dlopen(libraryPath.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!handle) {
throw Exception(string::f("Failed to load library %s: %s", libraryPath.c_str(), dlerror()));
}
#endif
return handle;
#if defined ARCH_WIN
SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS);
std::wstring libraryFilenameW = string::U8toU16(libraryPath);
HINSTANCE handle = LoadLibraryW(libraryFilenameW.c_str());
SetErrorMode(0);
if (!handle) {
int error = GetLastError();
throw Exception(string::f("Failed to load library %s: code %d", libraryPath.c_str(), error));
}
#else
// As of Rack v2.0, plugins are linked with `-rpath=.` so change current directory so it can find libRack.
std::string cwd = system::getWorkingDirectory();
system::setWorkingDirectory(asset::systemDir);
// Change it back when we're finished
DEFER({system::setWorkingDirectory(cwd);});
// Load library with dlopen
void* handle = dlopen(libraryPath.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!handle)
throw Exception(string::f("Failed to load library %s: %s", libraryPath.c_str(), dlerror()));
#endif
return handle;
}

typedef void (*InitCallback)(Plugin*);
@@ -85,9 +82,8 @@ static InitCallback loadPluginCallback(Plugin* plugin) {
std::string libraryPath = plugin->path + "/plugin." + libraryExt;

// Check file existence
if (!system::isFile(libraryPath)) {
throw Exception(string::f("Library %s does not exist", libraryPath.c_str()));
}
if (!system::isFile(libraryPath))
throw Exception(string::f("Plugin binary not found at %s", libraryPath.c_str()));

// Load dynamic/shared library
plugin->handle = loadLibrary(libraryPath);
@@ -99,9 +95,8 @@ static InitCallback loadPluginCallback(Plugin* plugin) {
#else
initCallback = (InitCallback) dlsym(plugin->handle, "init");
#endif
if (!initCallback) {
if (!initCallback)
throw Exception(string::f("Failed to read init() symbol in %s", libraryPath.c_str()));
}

return initCallback;
}
@@ -130,22 +125,16 @@ static Plugin* loadPlugin(std::string path) {

// Load plugin.json
std::string manifestFilename = (path == "") ? asset::system("Core.json") : (path + "/plugin.json");
FILE* file = fopen(manifestFilename.c_str(), "r");
if (!file) {
FILE* file = std::fopen(manifestFilename.c_str(), "r");
if (!file)
throw Exception(string::f("Manifest file %s does not exist", manifestFilename.c_str()));
}
DEFER({
fclose(file);
});
DEFER({std::fclose(file);});

json_error_t error;
json_t* rootJ = json_loadf(file, 0, &error);
if (!rootJ) {
if (!rootJ)
throw Exception(string::f("JSON parsing error at %s %d:%d %s", manifestFilename.c_str(), error.line, error.column, error.text));
}
DEFER({
json_decref(rootJ);
});
DEFER({json_decref(rootJ);});

// Call init callback
InitCallback initCallback;
@@ -162,12 +151,11 @@ static Plugin* loadPlugin(std::string path) {

// Reject plugin if slug already exists
Plugin* oldPlugin = getPlugin(plugin->slug);
if (oldPlugin) {
if (oldPlugin)
throw Exception(string::f("Plugin %s is already loaded, not attempting to load it again", plugin->slug.c_str()));
}

INFO("Loaded plugin %s v%s from %s", plugin->slug.c_str(), plugin->version.c_str(), plugin->path.c_str());
plugins.push_back(plugin);
INFO("Loaded plugin %s v%s from %s", plugin->slug.c_str(), plugin->version.c_str(), plugin->path.c_str());
}
catch (Exception& e) {
WARN("Could not load plugin %s: %s", path.c_str(), e.what());
@@ -193,7 +181,7 @@ static void extractPackages(std::string path) {
std::string message;

for (std::string packagePath : system::getEntries(path)) {
if (string::filenameExtension(string::filename(packagePath)) != "zip")
if (system::getExtension(packagePath) != ".zip")
continue;
INFO("Extracting package %s", packagePath.c_str());
// Extract package
@@ -201,14 +189,12 @@ static void extractPackages(std::string path) {
system::unarchiveToFolder(packagePath, path);
}
catch (Exception& e) {
WARN("Package %s failed to extract: %s", packagePath.c_str(), e.what());
message += string::f("Could not extract package %s\n", packagePath.c_str());
WARN("Plugin package %s failed to extract: %s", packagePath.c_str(), e.what());
message += string::f("Could not extract plugin package %s\n", packagePath.c_str());
continue;
}
// Remove package
if (remove(packagePath.c_str())) {
WARN("Could not delete file %s: error %d", packagePath.c_str(), errno);
}
system::remove(packagePath.c_str());
}
if (!message.empty()) {
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());


+ 0
- 49
src/string.cpp View File

@@ -87,55 +87,6 @@ bool endsWith(const std::string& str, const std::string& suffix) {
}


std::string directory(const std::string& path) {
char* pathDup = strdup(path.c_str());
std::string directory = dirname(pathDup);
free(pathDup);
return directory;
}


std::string filename(const std::string& path) {
char* pathDup = strdup(path.c_str());
std::string filename = basename(pathDup);
free(pathDup);
return filename;
}


std::string filenameBase(const std::string& filename) {
size_t pos = filename.rfind('.');
if (pos == std::string::npos)
return filename;
return std::string(filename, 0, pos);
}


std::string filenameExtension(const std::string& filename) {
size_t pos = filename.rfind('.');
if (pos == std::string::npos)
return "";
return std::string(filename, pos + 1);
}


std::string absolutePath(const std::string& path) {
#if defined ARCH_LIN || defined ARCH_MAC
char buf[PATH_MAX];
char* absPathC = realpath(path.c_str(), buf);
if (absPathC)
return absPathC;
#elif defined ARCH_WIN
std::wstring pathW = U8toU16(path);
wchar_t buf[PATH_MAX];
wchar_t* absPathC = _wfullpath(buf, pathW.c_str(), PATH_MAX);
if (absPathC)
return U16toU8(absPathC);
#endif
return "";
}


float fuzzyScore(const std::string& s, const std::string& query) {
size_t pos = s.find(query);
if (pos == std::string::npos)


+ 263
- 297
src/system.cpp View File

@@ -1,6 +1,7 @@
#include <thread>
#include <regex>
#include <chrono>
#include <experimental/filesystem>

#include <dirent.h>
#include <sys/stat.h>
@@ -33,39 +34,31 @@
#include <string.hpp>


namespace rack {
namespace system {
/*
In C++17, this will be `std::filesystem`
Important: When using `fs::path`, always convert strings to UTF-8 using

fs::path p = fs::u8path(s);

std::list<std::string> getEntries(const std::string& path) {
std::list<std::string> filenames;
DIR* dir = opendir(path.c_str());
if (dir) {
struct dirent* d;
while ((d = readdir(dir))) {
std::string filename = d->d_name;
if (filename == "." || filename == "..")
continue;
filenames.push_back(path + "/" + filename);
}
closedir(dir);
}
filenames.sort();
return filenames;
}
In fact, it's best to work only with strings to avoid forgetting to decode a string as UTF-8.
The need to do this is a fatal flaw of `fs::path`, but at least `std::filesystem` has some helpful operations.
*/
namespace fs = std::experimental::filesystem;


std::list<std::string> getEntriesRecursive(const std::string &path, int depth) {
std::list<std::string> entries = getEntries(path);
if (depth > 0) {
// Don't iterate using iterators because the list will be growing.
size_t limit = entries.size();
auto it = entries.begin();
for (size_t i = 0; i < limit; i++) {
const std::string &entry = *it++;
if (isDirectory(entry)) {
std::list<std::string> subEntries = getEntriesRecursive(entry, depth - 1);
// Append subEntries to entries
namespace rack {
namespace system {


std::list<std::string> getEntries(const std::string& dirPath, int depth) {
std::list<std::string> entries;
for (auto& entry : fs::directory_iterator(fs::u8path(dirPath))) {
std::string subEntry = entry.path().u8string();
entries.push_back(subEntry);
// Recurse if depth > 0 (limited recursion) or depth < 0 (infinite recursion).
if (depth != 0) {
if (fs::is_directory(entry.path())) {
std::list<std::string> subEntries = getEntries(subEntry, depth - 1);
entries.splice(entries.end(), subEntries);
}
}
@@ -74,311 +67,113 @@ std::list<std::string> getEntriesRecursive(const std::string &path, int depth) {
}


bool isFile(const std::string& path) {
struct stat statbuf;
if (stat(path.c_str(), &statbuf))
return false;
return S_ISREG(statbuf.st_mode);
bool doesExist(const std::string& path) {
return fs::exists(fs::u8path(path));
}


bool isDirectory(const std::string& path) {
struct stat statbuf;
if (stat(path.c_str(), &statbuf))
return false;
return S_ISDIR(statbuf.st_mode);
bool isFile(const std::string& path) {
return fs::is_regular_file(fs::u8path(path));
}


void moveFile(const std::string& srcPath, const std::string& destPath) {
std::remove(destPath.c_str());
// Whether this overwrites existing files is implementation-defined.
// i.e. Mingw64 fails to overwrite.
// This is why we remove the file above.
std::rename(srcPath.c_str(), destPath.c_str());
bool isDirectory(const std::string& path) {
return fs::is_directory(fs::u8path(path));
}


void copyFile(const std::string& srcPath, const std::string& destPath) {
// Open source
FILE* source = fopen(srcPath.c_str(), "rb");
if (!source)
return;
DEFER({
fclose(source);
});
// Open destination
FILE* dest = fopen(destPath.c_str(), "wb");
if (!dest)
return;
DEFER({
fclose(dest);
});
// Copy buffer
const int bufferSize = (1 << 15);
char buffer[bufferSize];
while (1) {
size_t size = fread(buffer, 1, bufferSize, source);
if (size == 0)
break;
size = fwrite(buffer, 1, size, dest);
if (size == 0)
break;
}
uint64_t getFileSize(const std::string& path) {
return fs::file_size(fs::u8path(path));
}


void createDirectory(const std::string& path) {
#if defined ARCH_WIN
std::wstring pathW = string::U8toU16(path);
_wmkdir(pathW.c_str());
#else
mkdir(path.c_str(), 0755);
#endif
void rename(const std::string& srcPath, const std::string& destPath) {
fs::rename(fs::u8path(srcPath), fs::u8path(destPath));
}


void createDirectories(const std::string& path) {
for (size_t i = 1; i < path.size(); i++) {
char c = path[i];
if (c == '/' || c == '\\')
createDirectory(path.substr(0, i));
}
createDirectory(path);
void copy(const std::string& srcPath, const std::string& destPath) {
fs::copy(fs::u8path(srcPath), fs::u8path(destPath), fs::copy_options::recursive);
}


void removeDirectory(const std::string& path) {
#if defined ARCH_WIN
std::wstring pathW = string::U8toU16(path);
_wrmdir(pathW.c_str());
#else
rmdir(path.c_str());
#endif
bool createDirectory(const std::string& path) {
return fs::create_directory(fs::u8path(path));
}


void removeDirectories(const std::string& path) {
removeDirectory(path);
for (size_t i = path.size() - 1; i >= 1; i--) {
char c = path[i];
if (c == '/' || c == '\\')
removeDirectory(path.substr(0, i));
}
bool createDirectories(const std::string& path) {
return fs::create_directories(fs::u8path(path));
}


std::string getWorkingDirectory() {
#if defined ARCH_WIN
wchar_t buf[4096] = L"";
GetCurrentDirectory(sizeof(buf), buf);
return string::U16toU8(buf);
#else
char buf[4096] = "";
getcwd(buf, sizeof(buf));
return buf;
#endif
bool remove(const std::string& path) {
return fs::remove(fs::u8path(path));
}


void setWorkingDirectory(const std::string& path) {
#if defined ARCH_WIN
std::wstring pathW = string::U8toU16(path);
SetCurrentDirectory(pathW.c_str());
#else
chdir(path.c_str());
#endif
}

int getLogicalCoreCount() {
return std::thread::hardware_concurrency();
int removeRecursively(const std::string& path) {
return fs::remove_all(fs::u8path(path));
}


void setThreadName(const std::string& name) {
#if defined ARCH_LIN
pthread_setname_np(pthread_self(), name.c_str());
#elif defined ARCH_WIN
// Unsupported on Windows
#endif
std::string getWorkingDirectory() {
return fs::current_path().u8string();
}


std::string getStackTrace() {
int stackLen = 128;
void* stack[stackLen];
std::string s;

#if defined ARCH_LIN || defined ARCH_MAC
stackLen = backtrace(stack, stackLen);
char** strings = backtrace_symbols(stack, stackLen);

// Skip the first line because it's this function.
for (int i = 1; i < stackLen; i++) {
s += string::f("%d: ", stackLen - i - 1);
std::string line = strings[i];
#if 0
// Parse line
std::regex r(R"((.*)\((.*)\+(.*)\) (.*))");
std::smatch match;
if (std::regex_search(line, match, r)) {
s += match[1].str();
s += "(";
std::string symbol = match[2].str();
// Demangle symbol
char* symbolD = __cxxabiv1::__cxa_demangle(symbol.c_str(), NULL, NULL, NULL);
if (symbolD) {
symbol = symbolD;
free(symbolD);
}
s += symbol;
s += "+";
s += match[3].str();
s += ")";
}
#else
s += line;
#endif
s += "\n";
}
free(strings);

#elif defined ARCH_WIN
HANDLE process = GetCurrentProcess();
SymInitialize(process, NULL, true);
stackLen = CaptureStackBackTrace(0, stackLen, stack, NULL);

SYMBOL_INFO* symbol = (SYMBOL_INFO*) calloc(sizeof(SYMBOL_INFO) + 256, 1);
symbol->MaxNameLen = 255;
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);

for (int i = 1; i < stackLen; i++) {
SymFromAddr(process, (DWORD64) stack[i], 0, symbol);
s += string::f("%d: %s 0x%0x\n", stackLen - i - 1, symbol->Name, symbol->Address);
}
free(symbol);
#endif

return s;
void setWorkingDirectory(const std::string& path) {
fs::current_path(fs::u8path(path));
}


int64_t getNanoseconds() {
#if defined ARCH_WIN
LARGE_INTEGER counter;
QueryPerformanceCounter(&counter);
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
// TODO Check if this is always an integer factor on all CPUs
int64_t nsPerTick = 1000000000LL / frequency.QuadPart;
int64_t time = counter.QuadPart * nsPerTick;
return time;
#endif
#if defined ARCH_LIN
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
int64_t time = int64_t(ts.tv_sec) * 1000000000LL + ts.tv_nsec;
return time;
#endif
#if defined ARCH_MAC
using clock = std::chrono::high_resolution_clock;
using time_point = std::chrono::time_point<clock>;
time_point now = clock::now();
using duration = std::chrono::duration<int64_t, std::nano>;
duration d = now.time_since_epoch();
return d.count();
#endif
std::string getTempDir() {
return fs::temp_directory_path().u8string();
}


void openBrowser(const std::string& url) {
#if defined ARCH_LIN
std::string command = "xdg-open \"" + url + "\"";
(void) std::system(command.c_str());
#endif
#if defined ARCH_MAC
std::string command = "open \"" + url + "\"";
std::system(command.c_str());
#endif
#if defined ARCH_WIN
std::wstring urlW = string::U8toU16(url);
ShellExecuteW(NULL, L"open", urlW.c_str(), NULL, NULL, SW_SHOWDEFAULT);
#endif
std::string getAbsolutePath(const std::string& path) {
return fs::absolute(fs::u8path(path)).u8string();
}


void openFolder(const std::string& path) {
#if defined ARCH_LIN
std::string command = "xdg-open \"" + path + "\"";
(void) std::system(command.c_str());
#endif
#if defined ARCH_MAC
std::string command = "open \"" + path + "\"";
std::system(command.c_str());
#endif
#if defined ARCH_WIN
std::wstring pathW = string::U8toU16(path);
ShellExecuteW(NULL, L"explore", pathW.c_str(), NULL, NULL, SW_SHOWDEFAULT);
#endif
std::string getDirectory(const std::string& path) {
return fs::u8path(path).parent_path().u8string();
}


void runProcessDetached(const std::string& path) {
#if defined ARCH_WIN
SHELLEXECUTEINFOW shExInfo;
ZeroMemory(&shExInfo, sizeof(shExInfo));
shExInfo.cbSize = sizeof(shExInfo);
shExInfo.lpVerb = L"runas";
std::string getFilename(const std::string& path) {
return fs::u8path(path).filename().u8string();
}

std::wstring pathW = string::U8toU16(path);
shExInfo.lpFile = pathW.c_str();
shExInfo.nShow = SW_SHOW;

if (ShellExecuteExW(&shExInfo)) {
// Do nothing
}
#else
// Not implemented on Linux or Mac
assert(0);
#endif
std::string getStem(const std::string& path) {
return fs::u8path(path).stem().u8string();
}


std::string getOperatingSystemInfo() {
#if defined ARCH_LIN || defined ARCH_MAC
struct utsname u;
uname(&u);
return string::f("%s %s %s %s", u.sysname, u.release, u.version, u.machine);
#elif defined ARCH_WIN
OSVERSIONINFOW info;
ZeroMemory(&info, sizeof(info));
info.dwOSVersionInfoSize = sizeof(info);
GetVersionExW(&info);
// See https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_osversioninfoa for a list of Windows version numbers.
return string::f("Windows %u.%u", info.dwMajorVersion, info.dwMinorVersion);
#endif
std::string getExtension(const std::string& path) {
return fs::u8path(path).extension().u8string();
}


/** Behaves like `std::filesystem::relative()`.
/** Returns `p` in relative path form, relative to `base`
Limitation: `p` must be a descendant of `base`. Doesn't support adding `../` to the return path.
*/
static filesystem::path getRelativePath(filesystem::path p, filesystem::path base = filesystem::current_path()) {
p = filesystem::absolute(p);
base = filesystem::absolute(base);
std::string pStr = p.generic_u8string();
std::string baseStr = base.generic_u8string();
if (pStr.size() < baseStr.size())
static std::string getRelativePath(std::string path, std::string base) {
path = fs::absolute(fs::u8path(path)).generic_u8string();
base = fs::absolute(fs::u8path(base)).generic_u8string();
if (path.size() < base.size())
throw Exception("getRelativePath() error: path is shorter than base");
if (!std::equal(baseStr.begin(), baseStr.end(), pStr.begin()))
if (!std::equal(base.begin(), base.end(), path.begin()))
throw Exception("getRelativePath() error: path does not begin with base");

// If p == base, this correctly returns "."
return "." + std::string(pStr.begin() + baseStr.size(), pStr.end());
// If path == base, this correctly returns "."
return "." + std::string(path.begin() + base.size(), path.end());
}


void archiveFolder(const filesystem::path& archivePath, const filesystem::path& folderPath) {
void archiveFolder(const std::string& archivePath, const std::string& folderPath) {
// Based on minitar.c create() in libarchive examples
int r;

@@ -388,24 +183,24 @@ void archiveFolder(const filesystem::path& archivePath, const filesystem::path&
archive_write_set_format_ustar(a);
archive_write_add_filter_zstd(a);
#if defined ARCH_WIN
r = archive_write_open_filename_w(a, archivePath.generic_wstring().c_str());
r = archive_write_open_filename_w(a, string::U8toU16(archivePath).c_str());
#else
r = archive_write_open_filename(a, archivePath.generic_u8string().c_str());
r = archive_write_open_filename(a, archivePath.c_str());
#endif
if (r < ARCHIVE_OK)
throw Exception(string::f("archiveFolder() could not open archive %s for writing: %s", archivePath.generic_u8string().c_str(), archive_error_string(a)));
throw Exception(string::f("archiveFolder() could not open archive %s for writing: %s", archivePath.c_str(), archive_error_string(a)));
DEFER({archive_write_close(a);});

// Open folder for reading
struct archive* disk = archive_read_disk_new();
DEFER({archive_read_free(disk);});
#if defined ARCH_WIN
r = archive_read_disk_open_w(disk, folderPath.generic_wstring().c_str());
r = archive_read_disk_open_w(disk, string::U8toU16(folderPath).c_str());
#else
r = archive_read_disk_open(disk, folderPath.generic_u8string().c_str());
r = archive_read_disk_open(disk, folderPath.c_str());
#endif
if (r < ARCHIVE_OK)
throw Exception(string::f("archiveFolder() could not open folder %s for reading: %s", folderPath.generic_u8string().c_str(), archive_error_string(a)));
throw Exception(string::f("archiveFolder() could not open folder %s for reading: %s", folderPath.c_str(), archive_error_string(a)));
DEFER({archive_read_close(a);});

// Iterate folder
@@ -423,17 +218,18 @@ void archiveFolder(const filesystem::path& archivePath, const filesystem::path&
archive_read_disk_descend(disk);

// Convert absolute path to relative path
filesystem::path entryPath =
std::string entryPath;
#if defined ARCH_WIN
archive_entry_pathname_w(entry);
entryPath = string::U16toU8(archive_entry_pathname_w(entry));
#else
archive_entry_pathname(entry);
entryPath = archive_entry_pathname(entry);
#endif
entryPath = getRelativePath(entryPath, folderPath);
#if defined ARCH_WIN
archive_entry_copy_pathname_w(entry, entryPath.generic_wstring().c_str());
// FIXME This doesn't seem to set UTF-8 paths on Windows.
archive_entry_copy_pathname_w(entry, string::U8toU16(entryPath).c_str());
#else
archive_entry_set_pathname(entry, entryPath.generic_u8string().c_str());
archive_entry_set_pathname(entry, entryPath.c_str());
#endif

// Write file to archive
@@ -443,11 +239,11 @@ void archiveFolder(const filesystem::path& archivePath, const filesystem::path&

// Manually copy data
#if defined ARCH_WIN
filesystem::path entrySourcePath = archive_entry_sourcepath_w(entry);
std::string entrySourcePath = string::U16toU8(archive_entry_sourcepath_w(entry));
#else
filesystem::path entrySourcePath = archive_entry_sourcepath(entry);
std::string entrySourcePath = archive_entry_sourcepath(entry);
#endif
FILE* f = std::fopen(entrySourcePath.generic_u8string().c_str(), "rb");
FILE* f = std::fopen(entrySourcePath.c_str(), "rb");
DEFER({std::fclose(f);});
char buf[1 << 14];
ssize_t len;
@@ -458,7 +254,7 @@ void archiveFolder(const filesystem::path& archivePath, const filesystem::path&
}


void unarchiveToFolder(const filesystem::path& archivePath, const filesystem::path& folderPath) {
void unarchiveToFolder(const std::string& archivePath, const std::string& folderPath) {
// Based on minitar.c extract() in libarchive examples
int r;

@@ -470,12 +266,12 @@ void unarchiveToFolder(const filesystem::path& archivePath, const filesystem::pa
archive_read_support_format_tar(a);
// archive_read_support_format_all(a);
#if defined ARCH_WIN
r = archive_read_open_filename_w(a, archivePath.generic_wstring().c_str(), 1 << 14);
r = archive_read_open_filename_w(a, string::U8toU16(archivePath).c_str(), 1 << 14);
#else
r = archive_read_open_filename(a, archivePath.generic_u8string().c_str(), 1 << 14);
r = archive_read_open_filename(a, archivePath.c_str(), 1 << 14);
#endif
if (r < ARCHIVE_OK)
throw Exception(string::f("unzipToFolder() could not open archive %s: %s", archivePath.generic_u8string().c_str(), archive_error_string(a)));
throw Exception(string::f("unzipToFolder() could not open archive %s: %s", archivePath.c_str(), archive_error_string(a)));
DEFER({archive_read_close(a);});

// Open folder for writing
@@ -496,14 +292,14 @@ void unarchiveToFolder(const filesystem::path& archivePath, const filesystem::pa
throw Exception(string::f("unzipToFolder() could not read entry from archive: %s", archive_error_string(a)));

// Convert relative pathname to absolute based on folderPath
filesystem::path entryPath = filesystem::u8path(archive_entry_pathname(entry));
if (!entryPath.is_relative())
throw Exception(string::f("unzipToFolder() does not support absolute paths: %s", entryPath.generic_u8string().c_str()));
entryPath = filesystem::absolute(entryPath, folderPath);
std::string entryPath = archive_entry_pathname(entry);
if (!fs::u8path(entryPath).is_relative())
throw Exception(string::f("unzipToFolder() does not support absolute paths: %s", entryPath.c_str()));
entryPath = fs::absolute(fs::u8path(entryPath), fs::u8path(folderPath)).u8string();
#if defined ARCH_WIN
archive_entry_copy_pathname_w(entry, entryPath.generic_wstring().c_str());
archive_entry_copy_pathname_w(entry, string::U8toU16(entryPath).c_str());
#else
archive_entry_set_pathname(entry, entryPath.generic_u8string().c_str());
archive_entry_set_pathname(entry, entryPath.c_str());
#endif

// Write entry to disk
@@ -537,5 +333,175 @@ void unarchiveToFolder(const filesystem::path& archivePath, const filesystem::pa
}


int getLogicalCoreCount() {
return std::thread::hardware_concurrency();
}


void setThreadName(const std::string& name) {
#if defined ARCH_LIN
pthread_setname_np(pthread_self(), name.c_str());
#elif defined ARCH_WIN
// Unsupported on Windows
#endif
}


std::string getStackTrace() {
int stackLen = 128;
void* stack[stackLen];
std::string s;

#if defined ARCH_LIN || defined ARCH_MAC
stackLen = backtrace(stack, stackLen);
char** strings = backtrace_symbols(stack, stackLen);

// Skip the first line because it's this function.
for (int i = 1; i < stackLen; i++) {
s += string::f("%d: ", stackLen - i - 1);
std::string line = strings[i];
#if 0
// Parse line
std::regex r(R"((.*)\((.*)\+(.*)\) (.*))");
std::smatch match;
if (std::regex_search(line, match, r)) {
s += match[1].str();
s += "(";
std::string symbol = match[2].str();
// Demangle symbol
char* symbolD = __cxxabiv1::__cxa_demangle(symbol.c_str(), NULL, NULL, NULL);
if (symbolD) {
symbol = symbolD;
free(symbolD);
}
s += symbol;
s += "+";
s += match[3].str();
s += ")";
}
#else
s += line;
#endif
s += "\n";
}
free(strings);

#elif defined ARCH_WIN
HANDLE process = GetCurrentProcess();
SymInitialize(process, NULL, true);
stackLen = CaptureStackBackTrace(0, stackLen, stack, NULL);

SYMBOL_INFO* symbol = (SYMBOL_INFO*) calloc(sizeof(SYMBOL_INFO) + 256, 1);
symbol->MaxNameLen = 255;
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);

for (int i = 1; i < stackLen; i++) {
SymFromAddr(process, (DWORD64) stack[i], 0, symbol);
s += string::f("%d: %s 0x%0x\n", stackLen - i - 1, symbol->Name, symbol->Address);
}
free(symbol);
#endif

return s;
}


int64_t getNanoseconds() {
#if defined ARCH_WIN
LARGE_INTEGER counter;
QueryPerformanceCounter(&counter);
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
// TODO Check if this is always an integer factor on all CPUs
int64_t nsPerTick = 1000000000LL / frequency.QuadPart;
int64_t time = counter.QuadPart * nsPerTick;
return time;
#endif
#if defined ARCH_LIN
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
int64_t time = int64_t(ts.tv_sec) * 1000000000LL + ts.tv_nsec;
return time;
#endif
#if defined ARCH_MAC
using clock = std::chrono::high_resolution_clock;
using time_point = std::chrono::time_point<clock>;
time_point now = clock::now();
using duration = std::chrono::duration<int64_t, std::nano>;
duration d = now.time_since_epoch();
return d.count();
#endif
}


std::string getOperatingSystemInfo() {
#if defined ARCH_LIN || defined ARCH_MAC
struct utsname u;
uname(&u);
return string::f("%s %s %s %s", u.sysname, u.release, u.version, u.machine);
#elif defined ARCH_WIN
OSVERSIONINFOW info;
ZeroMemory(&info, sizeof(info));
info.dwOSVersionInfoSize = sizeof(info);
GetVersionExW(&info);
// See https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_osversioninfoa for a list of Windows version numbers.
return string::f("Windows %u.%u", info.dwMajorVersion, info.dwMinorVersion);
#endif
}


void openBrowser(const std::string& url) {
#if defined ARCH_LIN
std::string command = "xdg-open \"" + url + "\"";
(void) std::system(command.c_str());
#endif
#if defined ARCH_MAC
std::string command = "open \"" + url + "\"";
std::system(command.c_str());
#endif
#if defined ARCH_WIN
std::wstring urlW = string::U8toU16(url);
ShellExecuteW(NULL, L"open", urlW.c_str(), NULL, NULL, SW_SHOWDEFAULT);
#endif
}


void openFolder(const std::string& path) {
#if defined ARCH_LIN
std::string command = "xdg-open \"" + path + "\"";
(void) std::system(command.c_str());
#endif
#if defined ARCH_MAC
std::string command = "open \"" + path + "\"";
std::system(command.c_str());
#endif
#if defined ARCH_WIN
std::wstring pathW = string::U8toU16(path);
ShellExecuteW(NULL, L"explore", pathW.c_str(), NULL, NULL, SW_SHOWDEFAULT);
#endif
}


void runProcessDetached(const std::string& path) {
#if defined ARCH_WIN
SHELLEXECUTEINFOW shExInfo;
ZeroMemory(&shExInfo, sizeof(shExInfo));
shExInfo.cbSize = sizeof(shExInfo);
shExInfo.lpVerb = L"runas";

std::wstring pathW = string::U8toU16(path);
shExInfo.lpFile = pathW.c_str();
shExInfo.nShow = SW_SHOW;

if (ShellExecuteExW(&shExInfo)) {
// Do nothing
}
#else
// Not implemented on Linux or Mac
assert(0);
#endif
}


} // namespace system
} // namespace rack

+ 2
- 1
src/updater.cpp View File

@@ -64,7 +64,8 @@ void update() {
return;

// Download update
std::string filename = string::filename(network::urlPath(downloadUrl));
// HACK getFilename is only supposed to be used for filesystem paths, not URLs.
std::string filename = system::getFilename(network::urlPath(downloadUrl));
std::string path = asset::user(filename);
INFO("Downloading update %s to %s", downloadUrl.c_str(), path.c_str());
network::requestDownload(downloadUrl, path, &progress);


+ 1
- 1
src/window.cpp View File

@@ -374,7 +374,7 @@ void Window::run() {
windowTitle += " - ";
if (!APP->history->isSaved())
windowTitle += "*";
windowTitle += string::filename(APP->patch->path);
windowTitle += system::getFilename(APP->patch->path);
}
if (windowTitle != internal->lastWindowTitle) {
glfwSetWindowTitle(win, windowTitle.c_str());


Loading…
Cancel
Save