Browse Source

Add a class to execute other processes. Only for Linux ATM

Panagiotis Christopoulos Charitos 5 years ago
parent
commit
419a7726e0

+ 1 - 1
LICENSE

@@ -1,5 +1,5 @@
 AnKi 3D Engine
-Copyright (c) 2009 - 2017 Panagiotis Christopoulos Charitos.
+Copyright (c) 2009 - 2020 Panagiotis Christopoulos Charitos.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without

+ 1 - 1
src/anki/renderer/ClusterBin.cpp

@@ -803,7 +803,7 @@ void ClusterBin::writeTypedObjectsToGpuBuffers(BinCtx& ctx) const
 			out.m_aabbMin = in.m_aabbMin;
 			out.m_aabbMax = in.m_aabbMax;
 			out.m_textureIndex = U32(&in - &rqueue.m_giProbes.getFront());
-			out.m_halfTexelSizeU = 1.0f / F32(in.m_cellCounts.x() * 6.0f) / 2.0f;
+			out.m_halfTexelSizeU = 1.0f / F32(F32(in.m_cellCounts.x()) * 6.0f) / 2.0f;
 			out.m_fadeDistance = in.m_fadeDistance;
 		}
 	}

+ 0 - 5
src/anki/util/Array.h

@@ -147,11 +147,6 @@ public:
 	ANKI_ARRAY_SIZE_METHOD(U64, N > MAX_U32)
 #undef ANKI_ARRAY_SIZE_METHOD
 
-	static constexpr PtrSize getSize()
-	{
-		return N;
-	}
-
 	/// Make it compatible with STL
 	static constexpr size_t size()
 	{

+ 2 - 2
src/anki/util/CMakeLists.txt

@@ -2,9 +2,9 @@ set(SOURCES Assert.cpp Functions.cpp File.cpp Filesystem.cpp Memory.cpp System.c
 	ThreadHive.cpp Hash.cpp Logger.cpp String.cpp StringList.cpp Tracer.cpp Serializer.cpp Xml.cpp)
 
 if(LINUX OR ANDROID OR MACOS)
-	set(SOURCES ${SOURCES} HighRezTimerPosix.cpp FilesystemPosix.cpp ThreadPosix.cpp)
+	set(SOURCES ${SOURCES} HighRezTimerPosix.cpp FilesystemPosix.cpp ThreadPosix.cpp ProcessPosix.cpp)
 else()
-	set(SOURCES ${SOURCES} HighRezTimerWindows.cpp FilesystemWindows.cpp ThreadWindows.cpp)
+	set(SOURCES ${SOURCES} HighRezTimerWindows.cpp FilesystemWindows.cpp ThreadWindows.cpp ProcessWindows.cpp)
 endif()
 
 if(LINUX)

+ 95 - 0
src/anki/util/Process.h

@@ -0,0 +1,95 @@
+// Copyright (C) 2009-2020, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#pragma once
+
+#include <anki/util/Array.h>
+#include <anki/util/String.h>
+#include <anki/util/WeakArray.h>
+
+namespace anki
+{
+
+/// @addtogroup util_system
+/// @{
+
+/// @memberof Process
+enum class ProcessStatus : U8
+{
+	RUNNING,
+	NOT_RUNNING,
+	NORMAL_EXIT,
+	CRASH_EXIT
+};
+
+/// @memberof Process
+enum class ProcessKillSignal : U8
+{
+	NORMAL,
+	FORCE
+};
+
+/// Executes external processes.
+class Process
+{
+public:
+	Process() = default;
+
+	~Process();
+
+	Process(const Process&) = delete;
+
+	Process& operator=(const Process&) = delete;
+
+	/// Start a process.
+	/// @param executable The executable to start.
+	/// @param arguments The command line arguments.
+	/// @param environment The environment variables.
+	ANKI_USE_RESULT Error start(
+		CString executable, ConstWeakArray<CString> arguments, ConstWeakArray<CString> environment);
+
+	/// Wait for the process to finish.
+	/// @param timeout The time to wait. If 0.0 wait forever.
+	/// @param[out] status The exit status
+	/// @param[out] exitCode The exit code if the process has finished.
+	ANKI_USE_RESULT Error wait(Second timeout = 0.0, ProcessStatus* status = nullptr, I32* exitCode = nullptr);
+
+	/// Get the status.
+	ANKI_USE_RESULT Error getStatus(ProcessStatus& status);
+
+	/// Kill the process.
+	ANKI_USE_RESULT Error kill(ProcessKillSignal k);
+
+	/// Read from stdout.
+	ANKI_USE_RESULT Error readFromStdout(StringAuto& text);
+
+	/// Read from stderr.
+	ANKI_USE_RESULT Error readFromStderr(StringAuto& text);
+
+private:
+#if ANKI_POSIX
+	static constexpr int DEFAULT_EXIT_CODE = -1;
+
+	int m_pid = -1;
+	int m_exitCode = DEFAULT_EXIT_CODE;
+	ProcessStatus m_status = ProcessStatus::NOT_RUNNING;
+	Array<int, 2> m_stdoutPipe = {{-1, -1}};
+	Array<int, 2> m_stderrPipe = {{-1, -1}};
+
+	void destroyPipes();
+
+	ANKI_USE_RESULT Error createPipes();
+
+	ANKI_USE_RESULT Error readFromFd(int fd, StringAuto& text) const;
+
+	/// Update some members.
+	ANKI_USE_RESULT Error refresh(int waitpidOptions);
+#else
+	// TODO
+#endif
+};
+/// @}
+
+} // end namespace anki

+ 304 - 0
src/anki/util/ProcessPosix.cpp

@@ -0,0 +1,304 @@
+// Copyright (C) 2009-2020, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#include <anki/util/Process.h>
+#include <anki/util/Functions.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <unistd.h>
+
+namespace anki
+{
+
+Process::~Process()
+{
+	ProcessStatus s;
+	const Error err = getStatus(s);
+	if(err)
+	{
+		ANKI_UTIL_LOGE("Failed to get the status. Expect cleanup errors");
+		return;
+	}
+
+	if(s == ProcessStatus::RUNNING)
+	{
+		ANKI_UTIL_LOGE("Destroying while child process is running");
+		return;
+	}
+
+	destroyPipes();
+}
+
+void Process::destroyPipes()
+{
+	for(U32 i = 0; i < 2; ++i)
+	{
+		close(m_stdoutPipe[i]);
+		close(m_stderrPipe[i]);
+		m_stdoutPipe[i] = m_stderrPipe[i] = -1;
+	}
+}
+
+Error Process::createPipes()
+{
+	destroyPipes();
+
+	if(pipe(m_stdoutPipe.getBegin()) || pipe(m_stderrPipe.getBegin()))
+	{
+		ANKI_UTIL_LOGE("pipe() failed: %s", strerror(errno));
+		return Error::FUNCTION_FAILED;
+	}
+
+	fcntl(m_stdoutPipe[0], F_SETFL, O_NONBLOCK);
+	fcntl(m_stderrPipe[0], F_SETFL, O_NONBLOCK);
+
+	return Error::NONE;
+}
+
+Error Process::start(CString executable, ConstWeakArray<CString> arguments, ConstWeakArray<CString> environment)
+{
+	ANKI_CHECK(refresh(0));
+	if(m_status == ProcessStatus::RUNNING)
+	{
+		ANKI_UTIL_LOGE("Process already running");
+		return Error::USER_DATA;
+	}
+
+	// Create the pipes
+	ANKI_CHECK(createPipes());
+
+	// Fork
+	m_pid = fork();
+	if(m_pid < 0)
+	{
+		ANKI_UTIL_LOGE("fork() failed: %s", strerror(errno));
+	}
+
+	if(m_pid == 0)
+	{
+		// Child
+
+		dup2(m_stdoutPipe[1], 1);
+		close(m_stdoutPipe[0]);
+		m_stdoutPipe[0] = -1;
+
+		dup2(m_stderrPipe[1], 2);
+		close(m_stderrPipe[0]);
+		m_stderrPipe[0] = -1;
+
+		// Set the args
+		char** cargs = static_cast<char**>(malloc((arguments.getSize() + 2) * sizeof(char*)));
+		cargs[0] = static_cast<char*>(malloc(executable.getLength() + 1));
+		strcpy(cargs[0], executable.cstr());
+		U32 i = 0;
+		for(; i < arguments.getSize(); ++i)
+		{
+			cargs[i + 1] = static_cast<char*>(malloc(arguments[i].getLength() + 1));
+			strcpy(cargs[i + 1], arguments[i].cstr());
+		}
+		cargs[i + 1] = nullptr;
+
+		// Set the env
+		char** newenv = static_cast<char**>(malloc((environment.getSize() + 1) * sizeof(char*)));
+		i = 0;
+		for(; i < environment.getSize(); ++i)
+		{
+			newenv[i] = static_cast<char*>(malloc(environment[i].getLength() + 1));
+			strcpy(newenv[i], environment[i].cstr());
+		}
+		newenv[i] = nullptr;
+
+		// Execute file
+		const int execerror = execvpe(executable.cstr(), cargs, newenv);
+		if(execerror)
+		{
+			printf("execvpe() failed: %s", strerror(errno));
+			exit(1);
+		}
+	}
+	else
+	{
+		close(m_stdoutPipe[1]);
+		m_stdoutPipe[1] = -1;
+		close(m_stderrPipe[1]);
+		m_stderrPipe[1] = -1;
+	}
+
+	return Error::NONE;
+}
+
+Error Process::kill(ProcessKillSignal k)
+{
+	if(m_pid < 1)
+	{
+		return Error::NONE;
+	}
+
+	int sig;
+	switch(k)
+	{
+	case ProcessKillSignal::NORMAL:
+		sig = SIGTERM;
+		break;
+	case ProcessKillSignal::FORCE:
+		sig = SIGKILL;
+		break;
+	default:
+		ANKI_ASSERT(0);
+	};
+
+	const pid_t p = ::kill(m_pid, sig);
+	if(p != 0)
+	{
+		ANKI_UTIL_LOGE("kill() failed: %s", strerror(errno));
+		return Error::FUNCTION_FAILED;
+	}
+
+	return Error::NONE;
+}
+
+Error Process::readFromFd(int fd, StringAuto& text) const
+{
+	fd_set readfds;
+	FD_ZERO(&readfds);
+	FD_SET(fd, &readfds);
+
+	timeval timeout;
+	timeout.tv_sec = 0; // Seconds
+	timeout.tv_usec = 10; // Microseconds
+
+	// Limit the iterations in case the process writes to FD constantly
+	U32 maxIterations = 16;
+	while(maxIterations--)
+	{
+		// Check if there are data
+		const int sel = select(1 + fd, &readfds, nullptr, nullptr, &timeout);
+		if(sel == 0)
+		{
+			// Timeout expired
+			break;
+		}
+		else if(sel == -1)
+		{
+			ANKI_UTIL_LOGE("select() failed: %s", strerror(errno));
+			return Error::FUNCTION_FAILED;
+		}
+
+		// Read the data
+		Array<char, 1024> buff;
+		const ssize_t bytesCount = read(fd, buff.getBegin(), buff.getSize() - 1);
+
+		if(bytesCount < 0 && errno != EAGAIN)
+		{
+			// NOTE errno == EINTR if a non-fatal signal arrived
+			ANKI_UTIL_LOGE("read() failed: %s", strerror(errno));
+			return Error::FUNCTION_FAILED;
+		}
+		else if(bytesCount == 0)
+		{
+			break;
+		}
+		else
+		{
+			buff[min<U32>(U32(bytesCount), buff.getSize() - 1)] = '\0';
+			text.append(&buff[0]);
+		}
+	}
+
+	return Error::NONE;
+}
+
+Error Process::readFromStdout(StringAuto& text)
+{
+	return readFromFd(m_stdoutPipe[0], text);
+}
+
+Error Process::readFromStderr(StringAuto& text)
+{
+	return readFromFd(m_stderrPipe[0], text);
+}
+
+Error Process::getStatus(ProcessStatus& status)
+{
+	ANKI_CHECK(refresh(WNOHANG));
+	status = m_status;
+	return Error::NONE;
+}
+
+Error Process::refresh(int waitpidOptions)
+{
+	Error err = Error::NONE;
+	m_status = ProcessStatus::NOT_RUNNING;
+	m_exitCode = DEFAULT_EXIT_CODE;
+
+	if(m_pid == -1)
+	{
+		m_status = ProcessStatus::NOT_RUNNING;
+		m_exitCode = DEFAULT_EXIT_CODE;
+	}
+	else
+	{
+		int status;
+		const pid_t p = waitpid(m_pid, &status, waitpidOptions);
+
+		if(p == -1)
+		{
+			m_status = ProcessStatus::NOT_RUNNING;
+			m_exitCode = DEFAULT_EXIT_CODE;
+			m_pid = -1;
+			ANKI_UTIL_LOGE("waitpid() failed: %s", strerror(errno));
+			err = Error::FUNCTION_FAILED;
+		}
+		else if(p == 0)
+		{
+			m_status = ProcessStatus::RUNNING;
+			m_exitCode = DEFAULT_EXIT_CODE;
+		}
+		else if(WIFEXITED(status))
+		{
+			m_status = ProcessStatus::NORMAL_EXIT;
+			m_exitCode = WEXITSTATUS(status);
+			m_pid = -1;
+		}
+		else if(WIFSIGNALED(status))
+		{
+			m_status = ProcessStatus::CRASH_EXIT;
+			m_exitCode = WTERMSIG(status);
+			m_pid = -1;
+		}
+		else if(WIFSTOPPED(status))
+		{
+			// NOTE child may resume later (and become a zombie)
+			m_status = ProcessStatus::CRASH_EXIT;
+			m_exitCode = WSTOPSIG(status);
+		}
+		else
+		{
+			ANKI_ASSERT(0);
+		}
+	}
+
+	return err;
+}
+
+Error Process::wait(Second timeout, ProcessStatus* status, I32* exitCode)
+{
+	ANKI_CHECK(refresh(0));
+
+	if(status)
+	{
+		*status = m_status;
+	}
+
+	if(exitCode)
+	{
+		*exitCode = m_exitCode;
+	}
+
+	return Error::NONE;
+}
+
+} // end namespace anki

+ 52 - 0
src/anki/util/ProcessWindows.cpp

@@ -0,0 +1,52 @@
+// Copyright (C) 2009-2020, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#include <anki/util/Process.h>
+
+namespace anki
+{
+
+Process::~Process()
+{
+	// TODO
+}
+
+Error Process::start(CString executable, ConstWeakArray<CString> arguments, ConstWeakArray<CString> environment)
+{
+	// TODO
+	return Error::NONE;
+}
+
+Error Process::kill(ProcessKillSignal k)
+{
+	// TODO
+	return Error::NONE;
+}
+
+Error Process::readFromStdout(StringAuto& text)
+{
+	// TODO
+	return Error::NONE;
+}
+
+Error Process::readFromStderr(StringAuto& text)
+{
+	// TODO
+	return Error::NONE;
+}
+
+Error Process::getStatus(ProcessStatus& status)
+{
+	// TODO
+	return Error::NONE;
+}
+
+Error Process::wait(Second timeout, ProcessStatus* status, I32* exitCode)
+{
+	// TODO
+	return Error::NONE;
+}
+
+} // end namespace anki

+ 1 - 1
src/anki/util/String.h

@@ -489,7 +489,7 @@ public:
 	{
 		auto size = m_data.getSize();
 		auto out = (size != 0) ? (size - 1) : 0;
-		ANKI_ASSERT(std::strlen(&m_data[0]) == out);
+		ANKI_ASSERT(size == 0 || std::strlen(&m_data[0]) == out);
 		return out;
 	}
 

+ 74 - 0
tests/util/Process.cpp

@@ -0,0 +1,74 @@
+// Copyright (C) 2009-2020, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#include <tests/framework/Framework.h>
+#include <anki/util/Process.h>
+#include <anki/util/File.h>
+#include <anki/util/HighRezTimer.h>
+
+ANKI_TEST(Util, Process)
+{
+	// Simple test
+	if(1)
+	{
+		Process proc;
+		ANKI_TEST_EXPECT_NO_ERR(proc.start("ls", Array<CString, 1>{{"-lt"}}, {}));
+		ANKI_TEST_EXPECT_NO_ERR(proc.wait());
+		HighRezTimer::sleep(1.0);
+
+		HeapAllocator<U8> alloc(allocAligned, nullptr);
+		StringAuto stdout(alloc);
+		ANKI_TEST_EXPECT_NO_ERR(proc.readFromStdout(stdout));
+		ANKI_TEST_LOGI(stdout.cstr());
+	}
+
+	// Stderr and stdout
+	if(1)
+	{
+		File file;
+		ANKI_TEST_EXPECT_NO_ERR(file.open("process_test.sh", FileOpenFlag::WRITE));
+		ANKI_TEST_EXPECT_NO_ERR(file.writeText(R"(#!/bin/bash
+x=1
+while [ $x -le 10 ]
+do
+	echo "Stdout message no $x"
+	echo "Stderr message no $x" 1>&2
+	sleep 1
+	x=$(( $x + 1 ))
+done
+)"));
+		file.close();
+
+		Process proc;
+		ANKI_TEST_EXPECT_NO_ERR(proc.start("bash", Array<CString, 1>{{"process_test.sh"}}, {}));
+		ProcessStatus status;
+
+		while(true)
+		{
+			ANKI_TEST_EXPECT_NO_ERR(proc.getStatus(status));
+			if(status == ProcessStatus::NORMAL_EXIT)
+			{
+				break;
+			}
+
+			HeapAllocator<U8> alloc(allocAligned, nullptr);
+			StringAuto stdout(alloc);
+			ANKI_TEST_EXPECT_NO_ERR(proc.readFromStdout(stdout));
+			if(stdout.getLength())
+			{
+				ANKI_TEST_LOGI(stdout.cstr());
+			}
+
+			StringAuto stderrStr(alloc);
+			ANKI_TEST_EXPECT_NO_ERR(proc.readFromStderr(stderrStr));
+			if(stderrStr.getLength())
+			{
+				ANKI_TEST_LOGI(stderrStr.cstr());
+			}
+		}
+
+		ANKI_TEST_EXPECT_NO_ERR(proc.wait(0.0, &status));
+	}
+}