PEBL 2.2
Psychology Experiment Building Language - Cross-platform psychological experiment development system
WorkspaceManager.cpp
Go to the documentation of this file.
1// WorkspaceManager.cpp - PEBL workspace initialization and management
2// Copyright (c) 2026 Shane T. Mueller
3// Licensed under GPL
4
5#include "WorkspaceManager.h"
6#include <sys/stat.h>
7#include <sys/types.h>
8#include <cstdlib>
9#include <cstring>
10#include <fstream>
11#include <iostream>
12#include <filesystem>
13
14namespace fs = std::filesystem;
15
16#ifdef _WIN32
17#include <windows.h>
18#include <direct.h>
19#include <shlobj.h>
20// Undef Windows CopyFile macro to avoid conflict with our function
21#ifdef CopyFile
22#undef CopyFile
23#endif
24#ifndef stat
25#define stat _stat
26#endif
27#define mkdir(path, mode) _mkdir(path)
28#ifndef S_ISDIR
29#define S_ISDIR(mode) (((mode) & _S_IFMT) == _S_IFDIR)
30#endif
31#else
32#include <dirent.h>
33#include <unistd.h>
34#include <pwd.h>
35#endif
36
38 : mInitialized(false)
39{
40 // In portable mode, use current directory; otherwise use Documents
41 if (IsPortableMode()) {
42 mWorkspacePath = GetPortableWorkspacePath();
43 } else {
44 mWorkspacePath = GetDocumentsPath() + "/pebl-exp." + PEBL_VERSION;
45 }
46}
47
50
52 // Check for STANDALONE.txt or PORTABLE.txt marker file.
53 // The marker should live at the portable distribution root, which is
54 // one level above bin\ on Windows (where the exe lives).
55 // We also accept it in ./ (same dir as CWD) in case the user places it
56 // next to the exe — GetPortableWorkspacePath() will still return ".."
57 // so studies are never created inside bin\.
58 if (FileExists("./STANDALONE.txt") || FileExists("../STANDALONE.txt") || FileExists("../../STANDALONE.txt")) {
59 return true;
60 }
61
62 if (FileExists("./PORTABLE.txt") || FileExists("../PORTABLE.txt") || FileExists("../../PORTABLE.txt")) {
63 return true;
64 }
65
66 // PEBL_PORTABLE environment variable (set by a batch file before launching)
67 const char* portableEnv = getenv("PEBL_PORTABLE");
68 if (portableEnv && strcmp(portableEnv, "1") == 0) {
69 return true;
70 }
71
72 return false;
73}
74
75std::string WorkspaceManager::GetPortableWorkspacePath() const {
76 // Determine the correct workspace root for portable mode.
77 // The workspace should be at the portable distribution root, not inside PEBL/bin/
78 //
79 // Expected structure:
80 // PEBL2.4_Portable/ <- workspace root (where STANDALONE.txt is)
81 // ├── STANDALONE.txt <- REQUIRED marker file
82 // ├── PEBL/
83 // │ ├── bin/
84 // │ │ └── pebl-launcher.exe
85 // │ ├── battery/
86 // │ └── pebl-lib/
87 // ├── my_studies/
88 // ├── chains/
89 // ├── snapshots/
90 // └── runPEBL.bat
91
92 // Determine workspace root based on marker file location.
93 //
94 // On Windows the launcher lives in bin\ and is typically launched from
95 // there (via shortcut or .bat), so CWD == the bin\ directory.
96 // STANDALONE.txt belongs at the *portable root* (one level up from bin\‍),
97 // never inside bin\ itself. If somehow it lands in CWD (./), treat that
98 // the same as one level up — workspace root is still "..".
99 //
100 // Priority: farthest-out marker wins (most specific to the root).
101
102 // Marker two levels up (e.g. CWD is PEBL/bin/ and root is ../../)
103 if (FileExists("../../STANDALONE.txt") || FileExists("../../PORTABLE.txt")) {
104 return "../..";
105 }
106
107 // Marker one level up (normal case: CWD is bin\, root is ..\‍)
108 if (FileExists("../STANDALONE.txt") || FileExists("../PORTABLE.txt")) {
109 return "..";
110 }
111
112 // Marker in CWD — user placed it next to the exe inside bin\.
113 // Treat workspace root as one level up so studies don't end up in bin\.
114 if (FileExists("./STANDALONE.txt") || FileExists("./PORTABLE.txt")) {
115 return "..";
116 }
117
118 // PEBL_PORTABLE env var: use parent of CWD (same assumption)
119 return "..";
120}
121
123 // In portable mode, workspace always "exists" (current directory)
124 if (IsPortableMode()) {
125 return false;
126 }
127
128 // Check if workspace directory exists and has content
129 if (!DirectoryExists(mWorkspacePath)) {
130 return true;
131 }
132
133 // Check if my_studies exists (primary indicator)
134 std::string studiesPath = mWorkspacePath + "/my_studies";
135 return !DirectoryExists(studiesPath);
136}
137
139 // Skip workspace creation in portable mode
140 if (IsPortableMode()) {
141 mInitialized = true;
142 return true;
143 }
144
145 // Create main workspace directory
146 if (!DirectoryExists(mWorkspacePath)) {
147 if (!CreateDir(mWorkspacePath)) {
148 return false;
149 }
150 }
151
152 // Create subdirectories
153 std::vector<std::string> subdirs = {
154 "/my_studies",
155 "/snapshots",
156 "/scales",
157 "/doc",
158 "/demo",
159 "/tutorials",
160 "/logs"
161 };
162
163 for (const auto& subdir : subdirs) {
164 std::string fullPath = mWorkspacePath + subdir;
165 if (!DirectoryExists(fullPath)) {
166 if (!CreateDir(fullPath)) {
167 return false;
168 }
169 }
170 }
171
172 mInitialized = true;
173 return true;
174}
175
176bool WorkspaceManager::CopyResources(const std::string& installationPath) {
177 if (installationPath.empty() || IsPortableMode()) {
178 std::cout << "CopyResources: skipped (empty path or portable mode)" << std::endl;
179 return false;
180 }
181
182 std::cout << "CopyResources: installation path = " << installationPath << std::endl;
183 std::cout << "CopyResources: workspace path = " << mWorkspacePath << std::endl;
184
185 // Copy specific documentation files
186 std::string docDest = mWorkspacePath + "/doc";
187 CreateDir(docDest);
188
189 std::vector<std::string> docFiles = {
190 std::string("doc/pman/PEBLManual") + PEBL_VERSION + ".pdf",
191 "doc/ReleaseNotes.txt",
192 "Notes_for_LLMs.txt"
193 };
194
195 std::cout << "Copying documentation files..." << std::endl;
196 for (const auto& relPath : docFiles) {
197 std::string srcFile = installationPath + "/" + relPath;
198 // Flatten the structure - put all files directly in doc/
199 std::string filename = relPath;
200 size_t lastSlash = filename.find_last_of('/');
201 if (lastSlash != std::string::npos) {
202 filename = filename.substr(lastSlash + 1);
203 }
204 std::string destFile = docDest + "/" + filename;
205
206 if (CopyFileContents(srcFile, destFile)) {
207 std::cout << " ✓ Copied " << filename << std::endl;
208 } else {
209 std::cout << " ✗ Failed to copy " << relPath << std::endl;
210 }
211 }
212
213 // Copy demos (entire directory, excluding tests subdirectory)
214 std::string demoSource = installationPath + "/demo";
215 std::string demoDest = mWorkspacePath + "/demo";
216 std::cout << "Checking for demo: " << demoSource << std::endl;
217 if (DirectoryExists(demoSource)) {
218 std::cout << "Copying demos from " << demoSource << " to " << demoDest << " (excluding tests/)" << std::endl;
219 std::vector<std::string> excludeDirs = {"tests"};
220 if (CopyDirectory(demoSource, demoDest, false, excludeDirs)) {
221 std::cout << " ✓ Demos copied successfully" << std::endl;
222 } else {
223 std::cout << " ✗ Failed to copy demos" << std::endl;
224 }
225 } else {
226 std::cout << " Demo source not found" << std::endl;
227 }
228
229 // Copy tutorials (entire directory)
230 std::string tutorialSource = installationPath + "/tutorials";
231 std::string tutorialDest = mWorkspacePath + "/tutorials";
232 std::cout << "Checking for tutorials: " << tutorialSource << std::endl;
233 if (DirectoryExists(tutorialSource)) {
234 std::cout << "Copying tutorials from " << tutorialSource << " to " << tutorialDest << std::endl;
235 if (CopyDirectory(tutorialSource, tutorialDest, false)) {
236 std::cout << " ✓ Tutorials copied successfully" << std::endl;
237 } else {
238 std::cout << " ✗ Failed to copy tutorials" << std::endl;
239 }
240 } else {
241 std::cout << " Tutorials source not found" << std::endl;
242 }
243
244 return true;
245}
246
248 return mInitialized || DirectoryExists(mWorkspacePath);
249}
250
251std::vector<std::string> WorkspaceManager::GetStudyDirectories() const {
252 std::vector<std::string> studies;
253 std::string studiesPath = GetStudiesPath();
254
255 try {
256 for (const auto& entry : fs::directory_iterator(studiesPath)) {
257 if (!entry.is_directory()) {
258 continue;
259 }
260
261 std::string dirName = entry.path().filename().string();
262 std::string fullPath = entry.path().string();
263
264 // Check if directory contains study-info.json
265 std::string studyInfoPath = fullPath + "/study-info.json";
266 struct stat fileInfo;
267 if (stat(studyInfoPath.c_str(), &fileInfo) == 0) {
268 studies.push_back(dirName);
269 }
270 }
271 } catch (const fs::filesystem_error&) {
272 // Directory doesn't exist or can't be read
273 }
274
275 return studies;
276}
277
278std::vector<std::string> WorkspaceManager::GetSnapshotDirectories() const {
279 std::vector<std::string> snapshots;
280 std::string snapshotsPath = GetSnapshotsPath();
281
282 try {
283 for (const auto& entry : fs::directory_iterator(snapshotsPath)) {
284 if (!entry.is_directory()) {
285 continue;
286 }
287
288 std::string dirName = entry.path().filename().string();
289 std::string fullPath = entry.path().string();
290
291 // Check if directory contains study-info.json
292 std::string studyInfoPath = fullPath + "/study-info.json";
293 struct stat fileInfo;
294 if (stat(studyInfoPath.c_str(), &fileInfo) == 0) {
295 snapshots.push_back(dirName);
296 }
297 }
298 } catch (const fs::filesystem_error&) {
299 // Directory doesn't exist or can't be read
300 }
301
302 return snapshots;
303}
304
305bool WorkspaceManager::CreateStudyDirectory(const std::string& studyName) {
306 std::string studyPath = GetStudiesPath() + "/" + studyName;
307
308 if (DirectoryExists(studyPath)) {
309 return false; // Already exists
310 }
311
312 // Create study directory
313 if (!CreateDir(studyPath)) {
314 return false;
315 }
316
317 // Create subdirectories
318 CreateDir(studyPath + "/chains");
319 CreateDir(studyPath + "/tests");
320 CreateDir(studyPath + "/data");
321
322 return true;
323}
324
325bool WorkspaceManager::CreateSnapshot(const std::string& studyPath, const std::string& snapshotName) {
326 std::string snapshotPath = GetSnapshotsPath() + "/" + snapshotName;
327
328 if (DirectoryExists(snapshotPath)) {
329 return false; // Already exists
330 }
331
332 // Copy study directory, excluding data/ subdirectories
333 return CopyDirectory(studyPath, snapshotPath, true);
334}
335
336bool WorkspaceManager::ImportSnapshot(const std::string& snapshotPath, const std::string& newStudyName) {
337 std::string studyPath = GetStudiesPath() + "/" + newStudyName;
338
339 if (DirectoryExists(studyPath)) {
340 return false; // Already exists
341 }
342
343 // Copy snapshot directory (includes everything since snapshots don't have data/)
344 return CopyDirectory(snapshotPath, studyPath, false);
345}
346
347bool WorkspaceManager::CreateDir(const std::string& path) {
348 struct stat info;
349 if (stat(path.c_str(), &info) == 0) {
350 return (info.st_mode & S_IFDIR) != 0; // Already exists
351 }
352
353 // Create directory with permissions 0755
354 if (mkdir(path.c_str(), 0755) == 0) {
355 return true;
356 }
357
358 // Try to create parent directories recursively
359 size_t pos = path.rfind('/');
360 if (pos != std::string::npos) {
361 std::string parentPath = path.substr(0, pos);
362 if (!parentPath.empty() && CreateDir(parentPath)) {
363 return mkdir(path.c_str(), 0755) == 0;
364 }
365 }
366
367 return false;
368}
369
370bool WorkspaceManager::DirectoryExists(const std::string& path) const {
371 struct stat info;
372 return (stat(path.c_str(), &info) == 0 && (info.st_mode & S_IFDIR));
373}
374
375bool WorkspaceManager::FileExists(const std::string& path) const {
376 struct stat info;
377 return (stat(path.c_str(), &info) == 0 && !(info.st_mode & S_IFDIR));
378}
379
380bool WorkspaceManager::CopyDirectory(const std::string& source, const std::string& dest, bool excludeData, const std::vector<std::string>& excludeDirs) {
381 // Create destination directory
382 if (!CreateDir(dest)) {
383 return false;
384 }
385
386 bool success = true;
387
388 try {
389 for (const auto& entry : fs::directory_iterator(source)) {
390 std::string name = entry.path().filename().string();
391
392 // Skip data/ directory if excludeData is true
393 if (excludeData && name == "data" && entry.is_directory()) {
394 continue;
395 }
396
397 // Skip directories in excludeDirs list
398 bool skipDir = false;
399 for (const auto& excludeDir : excludeDirs) {
400 if (name == excludeDir) {
401 skipDir = true;
402 break;
403 }
404 }
405 if (skipDir) {
406 continue;
407 }
408
409 std::string sourcePath = entry.path().string();
410 std::string destPath = dest + "/" + name;
411
412 if (entry.is_directory()) {
413 // Recursively copy directory
414 if (!CopyDirectory(sourcePath, destPath, excludeData, excludeDirs)) {
415 success = false;
416 break;
417 }
418 } else {
419 // Copy file
420 if (!CopyFileContents(sourcePath, destPath)) {
421 success = false;
422 break;
423 }
424 }
425 }
426 } catch (const fs::filesystem_error&) {
427 return false;
428 }
429
430 return success;
431}
432
433bool WorkspaceManager::CopyFileContents(const std::string& source, const std::string& dest) {
434 std::ifstream src(source, std::ios::binary);
435 if (!src.is_open()) {
436 return false;
437 }
438
439 std::ofstream dst(dest, std::ios::binary);
440 if (!dst.is_open()) {
441 return false;
442 }
443
444 dst << src.rdbuf();
445
446 src.close();
447 dst.close();
448
449 return true;
450}
451
452std::string WorkspaceManager::GetDocumentsPath() const {
453#ifdef _WIN32
454 // On Windows, use SHGetFolderPath to get Documents folder
455 char path[MAX_PATH];
456 if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, 0, path))) {
457 return std::string(path);
458 }
459 // Fallback to USERPROFILE/Documents
460 const char* userProfile = getenv("USERPROFILE");
461 if (userProfile) {
462 return std::string(userProfile) + "\\Documents";
463 }
464 return "C:\\Users\\Public\\Documents";
465#else
466 // Try to get Documents directory from environment or standard locations
467 const char* home = getenv("HOME");
468 if (home) {
469 // Check for XDG_DOCUMENTS_DIR first
470 std::string xdgConfigPath = std::string(home) + "/.config/user-dirs.dirs";
471 std::ifstream xdgConfig(xdgConfigPath);
472 if (xdgConfig.is_open()) {
473 std::string line;
474 while (std::getline(xdgConfig, line)) {
475 if (line.find("XDG_DOCUMENTS_DIR=") == 0) {
476 size_t start = line.find('"');
477 size_t end = line.rfind('"');
478 if (start != std::string::npos && end != std::string::npos && end > start) {
479 std::string docPath = line.substr(start + 1, end - start - 1);
480 // Replace $HOME with actual home path
481 size_t homePos = docPath.find("$HOME");
482 if (homePos != std::string::npos) {
483 docPath.replace(homePos, 5, home);
484 }
485 return docPath;
486 }
487 }
488 }
489 }
490
491 // Fallback to ~/Documents
492 return std::string(home) + "/Documents";
493 }
494
495 // Last resort: use /tmp but prefer nothing over a misleading path
496 // (if HOME is not set the launcher should still be usable)
497 return "/tmp/pebl-workspace";
498#endif
499}
#define NULL
Definition BinReloc.cpp:317
#define PEBL_VERSION
bool CreateSnapshot(const std::string &studyPath, const std::string &snapshotName)
bool CopyResources(const std::string &installationPath)
bool ImportSnapshot(const std::string &snapshotPath, const std::string &newStudyName)
std::string GetStudiesPath() const
bool IsInitialized() const
bool IsPortableMode() const
std::vector< std::string > GetSnapshotDirectories() const
std::vector< std::string > GetStudyDirectories() const
std::string GetSnapshotsPath() const
bool CreateStudyDirectory(const std::string &studyName)