PEBL 2.2
Psychology Experiment Building Language - Cross-platform psychological experiment development system
ExperimentRunner Class Reference

#include <ExperimentRunner.h>

Public Member Functions

 ExperimentRunner ()
 
 ExperimentRunner (LauncherConfig *config)
 
 ~ExperimentRunner ()
 
bool RunExperiment (const std::string &scriptPath, const std::vector< std::string > &args, const std::string &subjectCode="", const std::string &language="", bool fullscreen=false)
 
bool IsRunning ()
 
int WaitForCompletion ()
 
void Terminate ()
 
std::string GetStdout () const
 
std::string GetStderr () const
 
int GetExitCode () const
 
void UpdateOutput ()
 

Static Public Member Functions

static std::string GetLaunchLogPath ()
 

Detailed Description

Definition at line 14 of file ExperimentRunner.h.

Constructor & Destructor Documentation

◆ ExperimentRunner() [1/2]

ExperimentRunner::ExperimentRunner ( )

Definition at line 35 of file ExperimentRunner.cpp.

36 : mConfig(nullptr)
37 , mIsRunning(false)
38 , mLaunchTime(0)
39 , mCurrentFullscreen(false)
40 , mExitCode(-1)
41 , mProcessId(0)
42{
43#ifdef _WIN32
44 mProcessHandle = nullptr;
45 mStdoutReadPipe = nullptr;
46 mStdoutWritePipe = nullptr;
47 mStderrReadPipe = nullptr;
48 mStderrWritePipe = nullptr;
49#else
50 mStdoutPipe[0] = -1;
51 mStdoutPipe[1] = -1;
52 mStderrPipe[0] = -1;
53 mStderrPipe[1] = -1;
54#endif
55}

◆ ExperimentRunner() [2/2]

ExperimentRunner::ExperimentRunner ( LauncherConfig config)

Definition at line 57 of file ExperimentRunner.cpp.

58 : mConfig(config)
59 , mIsRunning(false)
60 , mLaunchTime(0)
61 , mCurrentFullscreen(false)
62 , mExitCode(-1)
63 , mProcessId(0)
64{
65#ifdef _WIN32
66 mProcessHandle = nullptr;
67 mStdoutReadPipe = nullptr;
68 mStdoutWritePipe = nullptr;
69 mStderrReadPipe = nullptr;
70 mStderrWritePipe = nullptr;
71#else
72 mStdoutPipe[0] = -1;
73 mStdoutPipe[1] = -1;
74 mStderrPipe[0] = -1;
75 mStderrPipe[1] = -1;
76#endif
77}

◆ ~ExperimentRunner()

ExperimentRunner::~ExperimentRunner ( )

Definition at line 79 of file ExperimentRunner.cpp.

80{
81 if (mIsRunning) {
82 Terminate();
83 }
84}

References Terminate().

Member Function Documentation

◆ GetExitCode()

int ExperimentRunner::GetExitCode ( ) const
inline

Definition at line 47 of file ExperimentRunner.h.

47{ return mExitCode; }

Referenced by LauncherUI::Render().

◆ GetLaunchLogPath()

std::string ExperimentRunner::GetLaunchLogPath ( )
static

Definition at line 475 of file ExperimentRunner.cpp.

476{
477 std::string logPath;
478
479#ifdef _WIN32
480 // Windows: Documents folder
481 char docPath[MAX_PATH];
482 if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, 0, docPath))) {
483 logPath = std::string(docPath);
484 }
485#else
486 // Linux/macOS: ~/Documents or ~ if Documents doesn't exist
487 const char* homeDir = getenv("HOME");
488 if (!homeDir) {
489 struct passwd* pw = getpwuid(getuid());
490 homeDir = pw->pw_dir;
491 }
492
493 std::string docDir = std::string(homeDir) + "/Documents";
494 struct stat st;
495 if (stat(docDir.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
496 logPath = docDir;
497 } else {
498 logPath = homeDir;
499 }
500#endif
501
502 // Look for pebl-exp.X.X directories (newest first)
503 const char* versions[] = {
504 "pebl-exp.2.4",
505 "pebl-exp.2.3",
506 "pebl-exp.2.2",
507 "pebl-exp.2.1",
508 "pebl-exp.2.0",
509 "pebl-exp.0.14",
510 "pebl-exp"
511 };
512
513#ifdef _WIN32
514 const char* separator = "\\";
515#else
516 const char* separator = "/";
517#endif
518
519 for (const char* version : versions) {
520 std::string peblPath = logPath + separator + version;
521 struct stat st;
522 if (stat(peblPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
523 // Check if logs subdirectory exists, create if needed
524 std::string logsPath = peblPath + separator + "logs";
525 struct stat logsSt;
526 if (stat(logsPath.c_str(), &logsSt) != 0 || !S_ISDIR(logsSt.st_mode)) {
527 // Create logs directory
528#ifdef _WIN32
529 CreateDirectoryA(logsPath.c_str(), NULL);
530#else
531 mkdir(logsPath.c_str(), 0755);
532#endif
533 }
534 return logsPath + separator + "launcher-log.csv";
535 }
536 }
537
538 // Fallback to home directory
539 return logPath + separator + "launcher-log.csv";
540}
#define NULL
Definition BinReloc.cpp:317

References NULL.

◆ GetStderr()

std::string ExperimentRunner::GetStderr ( ) const
inline

Definition at line 43 of file ExperimentRunner.h.

43{ return mStderrBuffer; }

Referenced by LauncherUI::Render().

◆ GetStdout()

std::string ExperimentRunner::GetStdout ( ) const
inline

Definition at line 42 of file ExperimentRunner.h.

42{ return mStdoutBuffer; }

Referenced by LauncherUI::Render().

◆ IsRunning()

bool ExperimentRunner::IsRunning ( )

Definition at line 691 of file ExperimentRunner.cpp.

692{
693 if (!mIsRunning) {
694 return false;
695 }
696
697#ifdef _WIN32
698 // Windows: Check if process handle is still valid and running
699 if (!mProcessHandle) {
700 mIsRunning = false;
701 return false;
702 }
703
704 // Read any available output while process is running
705 UpdateOutput();
706
707 DWORD exitCode;
708 if (GetExitCodeProcess(mProcessHandle, &exitCode)) {
709 if (exitCode == STILL_ACTIVE) {
710 return true;
711 } else {
712 // Process has finished
713 mExitCode = static_cast<int>(exitCode);
714 printf("Process %lu finished with exit code %d\n", mProcessId, mExitCode);
715 fflush(stdout);
716 // Also log to file for debugging
717 FILE* f = fopen("chain_debug.log", "a");
718 if (f) {
719 fprintf(f, "ExperimentRunner: Process finished, exit code = %d\n", mExitCode);
720 fclose(f);
721 }
722
723 // Do final read of any remaining output
724 UpdateOutput();
725
726 // Close pipe handles
727 if (mStdoutReadPipe) {
728 CloseHandle(static_cast<HANDLE>(mStdoutReadPipe));
729 mStdoutReadPipe = nullptr;
730 }
731 if (mStderrReadPipe) {
732 CloseHandle(static_cast<HANDLE>(mStderrReadPipe));
733 mStderrReadPipe = nullptr;
734 }
735
736 CloseHandle(mProcessHandle);
737 mProcessHandle = nullptr;
738 mIsRunning = false;
739 LogCompletion(mExitCode);
740 return false;
741 }
742 } else {
743 // Error querying process - assume it's dead
744 // Close pipe handles
745 if (mStdoutReadPipe) {
746 CloseHandle(static_cast<HANDLE>(mStdoutReadPipe));
747 mStdoutReadPipe = nullptr;
748 }
749 if (mStderrReadPipe) {
750 CloseHandle(static_cast<HANDLE>(mStderrReadPipe));
751 mStderrReadPipe = nullptr;
752 }
753 CloseHandle(mProcessHandle);
754 mProcessHandle = nullptr;
755 mIsRunning = false;
756 return false;
757 }
758#else
759 // Unix: Use waitpid with WNOHANG to check without blocking
760 // This is exactly what PEBL's CheckProcessStatus() does
761 int status;
762 pid_t result = waitpid(mProcessId, &status, WNOHANG);
763
764 if (result == 0) {
765 // Process still running
766 return true;
767 }
768 else if (result == mProcessId) {
769 // Process has finished
770 printf("Process %d finished\n", mProcessId);
771
772 // Do final read of any remaining output
773 UpdateOutput();
774
775 // Close pipes
776 if (mStdoutPipe[0] >= 0) {
777 close(mStdoutPipe[0]);
778 mStdoutPipe[0] = -1;
779 }
780 if (mStderrPipe[0] >= 0) {
781 close(mStderrPipe[0]);
782 mStderrPipe[0] = -1;
783 }
784
785 mIsRunning = false;
786
787 mExitCode = -1;
788 if (WIFEXITED(status)) {
789 mExitCode = WEXITSTATUS(status);
790 printf(" Exit code: %d\n", mExitCode);
791 } else if (WIFSIGNALED(status)) {
792 printf(" Terminated by signal %d\n", WTERMSIG(status));
793 }
794
795 LogCompletion(mExitCode);
796 return false;
797 }
798 else {
799 // Error or already reaped (result == -1)
800 printf("waitpid error for PID %d (already reaped or error)\n", mProcessId);
801 mIsRunning = false;
802 return false;
803 }
804#endif
805}

References UpdateOutput().

Referenced by LauncherUI::Render().

◆ RunExperiment()

bool ExperimentRunner::RunExperiment ( const std::string &  scriptPath,
const std::vector< std::string > &  args,
const std::string &  subjectCode = "",
const std::string &  language = "",
bool  fullscreen = false 
)

Definition at line 131 of file ExperimentRunner.cpp.

136{
137 if (mIsRunning) {
138 printf("Warning: Experiment already running\n");
139 return false;
140 }
141
142 std::string peblPath = GetPEBLExecutablePath();
143
144 // Extract directory from script path
145 std::string workingDir;
146 size_t lastSlash = scriptPath.find_last_of("/\\");
147 if (lastSlash != std::string::npos) {
148 workingDir = scriptPath.substr(0, lastSlash);
149 } else {
150 workingDir = ".";
151 }
152
153 // Get just the filename for passing to PEBL
154 std::string scriptFilename;
155 if (lastSlash != std::string::npos) {
156 scriptFilename = scriptPath.substr(lastSlash + 1);
157 } else {
158 scriptFilename = scriptPath;
159 }
160
161 // Build complete argument list with PEBL command-line flags
162 std::vector<std::string> fullArgs;
163
164 // Add user-provided args first (these may include -v for positional params)
165 for (const auto& arg : args) {
166 fullArgs.push_back(arg);
167 }
168
169 // Add subject code with -s flag
170 if (!subjectCode.empty()) {
171 fullArgs.push_back("-s");
172 fullArgs.push_back(subjectCode);
173 }
174
175 // Add language if specified
176 if (!language.empty()) {
177 fullArgs.push_back("--language");
178 fullArgs.push_back(language);
179 }
180
181 // Add fullscreen flag
182 if (fullscreen) {
183 fullArgs.push_back("--fullscreen");
184 }
185
186#ifdef _WIN32
187 // Windows: Use CreateProcess with pipes for stdout/stderr capture
188 // Normalize paths - convert forward slashes to backslashes for Windows API
189 std::string winPeblPath = NormalizeWindowsPath(peblPath);
190 std::string winWorkingDir = NormalizeWindowsPath(workingDir);
191
192 std::string cmdLine = "\"" + winPeblPath + "\" \"" + scriptFilename + "\"";
193
194 // Add all arguments
195 for (const auto& arg : fullArgs) {
196 cmdLine += " ";
197 if (arg.find(' ') != std::string::npos) {
198 cmdLine += "\"" + arg + "\"";
199 } else {
200 cmdLine += arg;
201 }
202 }
203
204 printf("Windows CreateProcess: cmd=%s, workdir=%s\n", cmdLine.c_str(), winWorkingDir.c_str());
205
206 // Clear output buffers
207 mStdoutBuffer.clear();
208 mStderrBuffer.clear();
209
210 // Set up security attributes for pipe inheritance
211 SECURITY_ATTRIBUTES sa;
212 sa.nLength = sizeof(SECURITY_ATTRIBUTES);
213 sa.bInheritHandle = TRUE; // Child process inherits handles
214 sa.lpSecurityDescriptor = NULL;
215
216 // Create stdout pipe
217 HANDLE hStdoutRead, hStdoutWrite;
218 if (!CreatePipe(&hStdoutRead, &hStdoutWrite, &sa, 0)) {
219 printf("Failed to create stdout pipe: %lu\n", GetLastError());
220 return false;
221 }
222 // Ensure read end is not inherited by child
223 SetHandleInformation(hStdoutRead, HANDLE_FLAG_INHERIT, 0);
224
225 // Create stderr pipe
226 HANDLE hStderrRead, hStderrWrite;
227 if (!CreatePipe(&hStderrRead, &hStderrWrite, &sa, 0)) {
228 printf("Failed to create stderr pipe: %lu\n", GetLastError());
229 CloseHandle(hStdoutRead);
230 CloseHandle(hStdoutWrite);
231 return false;
232 }
233 // Ensure read end is not inherited by child
234 SetHandleInformation(hStderrRead, HANDLE_FLAG_INHERIT, 0);
235
236 // Store pipe handles
237 mStdoutReadPipe = hStdoutRead;
238 mStdoutWritePipe = hStdoutWrite;
239 mStderrReadPipe = hStderrRead;
240 mStderrWritePipe = hStderrWrite;
241
242 STARTUPINFOA si;
243 PROCESS_INFORMATION pi;
244 ZeroMemory(&si, sizeof(si));
245 si.cb = sizeof(si);
246 si.hStdOutput = hStdoutWrite;
247 si.hStdError = hStderrWrite;
248 si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
249 si.dwFlags |= STARTF_USESTDHANDLES;
250 ZeroMemory(&pi, sizeof(pi));
251
252 // Create the child process with working directory set
253 if (!CreateProcessA(
254 NULL, // Application name
255 const_cast<char*>(cmdLine.c_str()), // Command line
256 NULL, // Process security attributes
257 NULL, // Thread security attributes
258 TRUE, // Inherit handles (must be TRUE for pipes)
259 0, // Creation flags
260 NULL, // Environment
261 winWorkingDir.c_str(), // Current directory (IMPORTANT!) - must use backslashes
262 &si, // Startup info
263 &pi)) // Process info
264 {
265 printf("Failed to create process: %s\n", cmdLine.c_str());
266 printf("Working directory was: %s\n", winWorkingDir.c_str());
267 printf("GetLastError: %lu\n", GetLastError());
268 // Clean up pipe handles
269 CloseHandle(hStdoutRead);
270 CloseHandle(hStdoutWrite);
271 CloseHandle(hStderrRead);
272 CloseHandle(hStderrWrite);
273 mStdoutReadPipe = nullptr;
274 mStdoutWritePipe = nullptr;
275 mStderrReadPipe = nullptr;
276 mStderrWritePipe = nullptr;
277 return false;
278 }
279
280 mProcessHandle = pi.hProcess;
281 mProcessId = pi.dwProcessId;
282 CloseHandle(pi.hThread);
283
284 // Close write ends in parent - child has copies
285 CloseHandle(hStdoutWrite);
286 CloseHandle(hStderrWrite);
287 mStdoutWritePipe = nullptr;
288 mStderrWritePipe = nullptr;
289
290 mIsRunning = true;
291
292#else
293 // Unix: Create pipes for stdout/stderr capture
294 if (pipe(mStdoutPipe) < 0 || pipe(mStderrPipe) < 0) {
295 printf("Failed to create pipes\n");
296 return false;
297 }
298
299 // Make read ends non-blocking
300 fcntl(mStdoutPipe[0], F_SETFL, O_NONBLOCK);
301 fcntl(mStderrPipe[0], F_SETFL, O_NONBLOCK);
302
303 // Clear output buffers
304 mStdoutBuffer.clear();
305 mStderrBuffer.clear();
306
307 pid_t pid = fork();
308
309 if (pid < 0) {
310 printf("Failed to fork process\n");
311 close(mStdoutPipe[0]);
312 close(mStdoutPipe[1]);
313 close(mStderrPipe[0]);
314 close(mStderrPipe[1]);
315 return false;
316 }
317
318 if (pid == 0) {
319 // Child process - redirect stdout/stderr to pipes
320 close(mStdoutPipe[0]); // Close read ends
321 close(mStderrPipe[0]);
322
323 dup2(mStdoutPipe[1], STDOUT_FILENO);
324 dup2(mStderrPipe[1], STDERR_FILENO);
325
326 close(mStdoutPipe[1]);
327 close(mStderrPipe[1]);
328
329 // Change to working directory
330 if (chdir(workingDir.c_str()) != 0) {
331 fprintf(stderr, "Failed to change directory to: %s\n", workingDir.c_str());
332 exit(1);
333 }
334
335 std::vector<char*> argv;
336 argv.push_back(const_cast<char*>(peblPath.c_str()));
337 argv.push_back(const_cast<char*>(scriptFilename.c_str()));
338
339 for (const auto& arg : fullArgs) {
340 argv.push_back(const_cast<char*>(arg.c_str()));
341 }
342
343 argv.push_back(nullptr);
344
345 execvp(peblPath.c_str(), argv.data());
346
347 // If we get here, exec failed
348 fprintf(stderr, "Failed to execute: %s\n", peblPath.c_str());
349 exit(1);
350 }
351
352 // Parent process - close write ends
353 close(mStdoutPipe[1]);
354 close(mStderrPipe[1]);
355 mStdoutPipe[1] = -1;
356 mStderrPipe[1] = -1;
357
358 mProcessId = pid;
359 mIsRunning = true;
360#endif
361
362 printf("Launched experiment: %s (PID: %lu) in directory: %s\n",
363 scriptFilename.c_str(), (unsigned long)mProcessId, workingDir.c_str());
364
365 // Log the launch
366 mCurrentScript = scriptPath;
367 mCurrentSubject = subjectCode;
368 mCurrentLanguage = language;
369 mCurrentFullscreen = fullscreen;
370 mLaunchTime = std::time(nullptr);
371 LogLaunch(scriptPath, subjectCode, language, fullscreen);
372
373 return true;
374}

References NULL.

◆ Terminate()

void ExperimentRunner::Terminate ( )

Definition at line 437 of file ExperimentRunner.cpp.

438{
439 if (!mIsRunning) {
440 return;
441 }
442
443#ifdef _WIN32
444 TerminateProcess(mProcessHandle, 1);
445 CloseHandle(mProcessHandle);
446 mProcessHandle = nullptr;
447 // Close pipe handles
448 if (mStdoutReadPipe) {
449 CloseHandle(static_cast<HANDLE>(mStdoutReadPipe));
450 mStdoutReadPipe = nullptr;
451 }
452 if (mStderrReadPipe) {
453 CloseHandle(static_cast<HANDLE>(mStderrReadPipe));
454 mStderrReadPipe = nullptr;
455 }
456#else
457 kill(mProcessId, SIGTERM);
458
459 // Close pipes
460 if (mStdoutPipe[0] >= 0) {
461 close(mStdoutPipe[0]);
462 mStdoutPipe[0] = -1;
463 }
464 if (mStderrPipe[0] >= 0) {
465 close(mStderrPipe[0]);
466 mStderrPipe[0] = -1;
467 }
468#endif
469
470 mIsRunning = false;
471 LogCompletion(-999); // Log termination with special exit code
472 printf("Terminated experiment (PID: %lu)\n", mProcessId);
473}

Referenced by ~ExperimentRunner().

◆ UpdateOutput()

void ExperimentRunner::UpdateOutput ( )

Definition at line 632 of file ExperimentRunner.cpp.

633{
634#ifdef _WIN32
635 // Read from stdout pipe (non-blocking using PeekNamedPipe)
636 if (mStdoutReadPipe) {
637 HANDLE hPipe = static_cast<HANDLE>(mStdoutReadPipe);
638 DWORD bytesAvailable = 0;
639 while (PeekNamedPipe(hPipe, NULL, 0, NULL, &bytesAvailable, NULL) && bytesAvailable > 0) {
640 char buffer[4096];
641 DWORD bytesToRead = (bytesAvailable < sizeof(buffer) - 1) ? bytesAvailable : sizeof(buffer) - 1;
642 DWORD bytesRead = 0;
643 if (ReadFile(hPipe, buffer, bytesToRead, &bytesRead, NULL) && bytesRead > 0) {
644 buffer[bytesRead] = '\0';
645 mStdoutBuffer.append(buffer);
646 } else {
647 break;
648 }
649 }
650 }
651
652 // Read from stderr pipe (non-blocking using PeekNamedPipe)
653 if (mStderrReadPipe) {
654 HANDLE hPipe = static_cast<HANDLE>(mStderrReadPipe);
655 DWORD bytesAvailable = 0;
656 while (PeekNamedPipe(hPipe, NULL, 0, NULL, &bytesAvailable, NULL) && bytesAvailable > 0) {
657 char buffer[4096];
658 DWORD bytesToRead = (bytesAvailable < sizeof(buffer) - 1) ? bytesAvailable : sizeof(buffer) - 1;
659 DWORD bytesRead = 0;
660 if (ReadFile(hPipe, buffer, bytesToRead, &bytesRead, NULL) && bytesRead > 0) {
661 buffer[bytesRead] = '\0';
662 mStderrBuffer.append(buffer);
663 } else {
664 break;
665 }
666 }
667 }
668#else
669 // Read from stdout pipe (non-blocking)
670 if (mStdoutPipe[0] >= 0) {
671 char buffer[4096];
672 ssize_t bytesRead;
673 while ((bytesRead = read(mStdoutPipe[0], buffer, sizeof(buffer) - 1)) > 0) {
674 buffer[bytesRead] = '\0';
675 mStdoutBuffer.append(buffer);
676 }
677 }
678
679 // Read from stderr pipe (non-blocking)
680 if (mStderrPipe[0] >= 0) {
681 char buffer[4096];
682 ssize_t bytesRead;
683 while ((bytesRead = read(mStderrPipe[0], buffer, sizeof(buffer) - 1)) > 0) {
684 buffer[bytesRead] = '\0';
685 mStderrBuffer.append(buffer);
686 }
687 }
688#endif
689}

References NULL.

Referenced by IsRunning(), LauncherUI::Render(), and WaitForCompletion().

◆ WaitForCompletion()

int ExperimentRunner::WaitForCompletion ( )

Definition at line 376 of file ExperimentRunner.cpp.

377{
378 if (!mIsRunning) {
379 return -1;
380 }
381
382#ifdef _WIN32
383 WaitForSingleObject(mProcessHandle, INFINITE);
384
385 // Do final read of any remaining output
386 UpdateOutput();
387
388 // Close pipe read handles
389 if (mStdoutReadPipe) {
390 CloseHandle(static_cast<HANDLE>(mStdoutReadPipe));
391 mStdoutReadPipe = nullptr;
392 }
393 if (mStderrReadPipe) {
394 CloseHandle(static_cast<HANDLE>(mStderrReadPipe));
395 mStderrReadPipe = nullptr;
396 }
397
398 DWORD exitCode;
399 GetExitCodeProcess(mProcessHandle, &exitCode);
400 mExitCode = static_cast<int>(exitCode);
401 CloseHandle(mProcessHandle);
402
403 mProcessHandle = nullptr;
404 mIsRunning = false;
405
406 LogCompletion(mExitCode);
407 return mExitCode;
408#else
409 int status;
410 waitpid(mProcessId, &status, 0);
411
412 // Do final read of any remaining output
413 UpdateOutput();
414
415 // Close pipes
416 if (mStdoutPipe[0] >= 0) {
417 close(mStdoutPipe[0]);
418 mStdoutPipe[0] = -1;
419 }
420 if (mStderrPipe[0] >= 0) {
421 close(mStderrPipe[0]);
422 mStderrPipe[0] = -1;
423 }
424
425 mIsRunning = false;
426
427 mExitCode = -1;
428 if (WIFEXITED(status)) {
429 mExitCode = WEXITSTATUS(status);
430 }
431
432 LogCompletion(mExitCode);
433 return mExitCode;
434#endif
435}

References UpdateOutput().


The documentation for this class was generated from the following files: