v1.9.0 #175
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,7 +9,7 @@
|
||||
!misc/eclipse/**/.project
|
||||
|
||||
#vscode
|
||||
.vscode
|
||||
/.vscode
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
|
@ -160,8 +160,7 @@ class CoreController : public ExtendedControllerBase {
|
||||
sd::SdState currentlyCommandedState = sd::SdState::OFF;
|
||||
sd::SdCard commandedCard = sd::SdCard::NONE;
|
||||
sd::SdState commandedState = sd::SdState::OFF;
|
||||
};
|
||||
SdInfo sdInfo;
|
||||
} sdInfo;
|
||||
RebootFile rebootFile = {};
|
||||
bool doPerformRebootFileHandling = true;
|
||||
|
||||
|
3
unittest/rebootLogic/.vscode/settings.json
vendored
Normal file
3
unittest/rebootLogic/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.tabSize": 2
|
||||
}
|
@ -6,6 +6,18 @@ enable_testing()
|
||||
|
||||
add_executable(reboot-logic main.cpp)
|
||||
|
||||
target_sources(reboot-logic PRIVATE
|
||||
main.cpp
|
||||
CoreController.cpp
|
||||
SdCardManager.cpp
|
||||
event.cpp
|
||||
libxiphos.cpp
|
||||
)
|
||||
|
||||
target_include_directories(reboot-logic PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
|
||||
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
|
||||
include(CPack)
|
||||
|
376
unittest/rebootLogic/CoreController.cpp
Normal file
376
unittest/rebootLogic/CoreController.cpp
Normal file
@ -0,0 +1,376 @@
|
||||
#include "conf.h"
|
||||
#include "CoreController.h"
|
||||
#include "SdCardManager.h"
|
||||
#include "event.h"
|
||||
#include "libxiphos.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
xsc::Chip CoreController::CURRENT_CHIP = xsc::Chip::NO_CHIP;
|
||||
xsc::Copy CoreController::CURRENT_COPY = xsc::Copy::NO_COPY;
|
||||
|
||||
CoreController::CoreController() {
|
||||
sdcMan = new SdCardManager();
|
||||
}
|
||||
|
||||
void CoreController::performRebootFileHandling(bool recreateFile) {
|
||||
bool sdCardMounted = false;
|
||||
if (not recreateFile and doPerformRebootFileHandling) {
|
||||
sdCardMounted = sdcMan->isSdCardMounted(sdInfo.pref);
|
||||
}
|
||||
if ((doPerformRebootFileHandling and sdCardMounted) or recreateFile) {
|
||||
using namespace std;
|
||||
if (recreateFile) {
|
||||
#if OBSW_VERBOSE_LEVEL >= 1
|
||||
std::cout << "CoreController::performRebootFileHandling: Recreating reboot file" << std::endl;
|
||||
#endif
|
||||
}
|
||||
std::string path = sdcMan->getCurrentMountPrefix(sdInfo.pref) + REBOOT_FILE;
|
||||
if (not std::filesystem::exists(path) or recreateFile) {
|
||||
rebootFile.enabled = true;
|
||||
rebootFile.img00Cnt = 0;
|
||||
rebootFile.img01Cnt = 0;
|
||||
rebootFile.img10Cnt = 0;
|
||||
rebootFile.img11Cnt = 0;
|
||||
rebootFile.lastChip = xsc::Chip::CHIP_0;
|
||||
rebootFile.lastCopy = xsc::Copy::COPY_0;
|
||||
rebootFile.bootFlag = false;
|
||||
rewriteRebootFile(rebootFile);
|
||||
} else {
|
||||
if (not parseRebootFile(path, rebootFile)) {
|
||||
performRebootFileHandling(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (rebootFile.bootFlag) {
|
||||
// Trigger event to inform ground that a reboot was triggered
|
||||
uint32_t p1 = rebootFile.lastChip << 16 | rebootFile.lastCopy;
|
||||
uint32_t p2 = rebootFile.img00Cnt << 24 | rebootFile.img01Cnt << 16 |
|
||||
rebootFile.img10Cnt << 8 | rebootFile.img11Cnt;
|
||||
triggerEvent(REBOOT_MECHANISM_TRIGGERED, p1, p2);
|
||||
// Clear the boot flag
|
||||
rebootFile.bootFlag = false;
|
||||
}
|
||||
|
||||
switch (CURRENT_CHIP) {
|
||||
case (xsc::CHIP_0): {
|
||||
switch (CURRENT_COPY) {
|
||||
case (xsc::COPY_0): {
|
||||
rebootFile.img00Cnt++;
|
||||
break;
|
||||
}
|
||||
case (xsc::COPY_1): {
|
||||
rebootFile.img01Cnt++;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case (xsc::CHIP_1): {
|
||||
switch (CURRENT_COPY) {
|
||||
case (xsc::COPY_0): {
|
||||
rebootFile.img10Cnt++;
|
||||
break;
|
||||
}
|
||||
case (xsc::COPY_1): {
|
||||
rebootFile.img11Cnt++;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (CURRENT_CHIP == xsc::CHIP_0 and CURRENT_COPY == xsc::COPY_0) {
|
||||
}
|
||||
if (rebootFile.relevantBootCnt > rebootFile.maxCount) {
|
||||
// Reboot to other image
|
||||
bool doReboot = false;
|
||||
determineAndExecuteReboot(rebootFile, doReboot, rebootFile.lastChip, rebootFile.lastCopy);
|
||||
if (doReboot) {
|
||||
rebootFile.bootFlag = true;
|
||||
#if OBSW_VERBOSE_LEVEL >= 1
|
||||
std::cout << "Boot counter for image " << CURRENT_CHIP << " " << CURRENT_COPY
|
||||
<< "too high. Rebooting to " << rebootFile.lastChip << " " << rebootFile.lastCopy
|
||||
<< std::endl;
|
||||
#endif
|
||||
rewriteRebootFile(rebootFile);
|
||||
xsc_boot_copy(static_cast<xsc_libnor_chip_t>(rebootFile.lastChip),
|
||||
static_cast<xsc_libnor_copy_t>(rebootFile.lastCopy));
|
||||
}
|
||||
} else {
|
||||
rewriteRebootFile(rebootFile);
|
||||
}
|
||||
doPerformRebootFileHandling = false;
|
||||
}
|
||||
}
|
||||
|
||||
void CoreController::determineAndExecuteReboot(RebootFile &rf, bool &needsReboot,
|
||||
xsc::Chip &tgtChip, xsc::Copy &tgtCopy) {
|
||||
tgtChip = xsc::CHIP_0;
|
||||
tgtCopy = xsc::COPY_0;
|
||||
needsReboot = false;
|
||||
if ((CURRENT_COPY == xsc::COPY_0) and (CURRENT_CHIP == xsc::CHIP_0) and
|
||||
(rf.img00Cnt >= rf.maxCount)) {
|
||||
needsReboot = true;
|
||||
if (rf.img01Cnt >= rf.maxCount) {
|
||||
if (rf.img10Cnt >= rf.maxCount) {
|
||||
if (rf.img11Cnt >= rf.maxCount) {
|
||||
// Can't really do much here. Stay on image
|
||||
std::cout << "All reboot counts too high, but already on fallback image" << std::endl;
|
||||
return;
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_1;
|
||||
tgtCopy = xsc::COPY_1;
|
||||
}
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_0;
|
||||
tgtCopy = xsc::COPY_1;
|
||||
}
|
||||
} else {
|
||||
tgtCopy = xsc::COPY_1;
|
||||
}
|
||||
}
|
||||
if ((CURRENT_COPY == xsc::COPY_0) and (CURRENT_CHIP == xsc::CHIP_1) and
|
||||
(rf.img01Cnt >= rf.maxCount)) {
|
||||
needsReboot = true;
|
||||
if (rf.img00Cnt >= rf.maxCount) {
|
||||
if (rf.img10Cnt >= rf.maxCount) {
|
||||
if (rf.img11Cnt >= rf.maxCount) {
|
||||
// Reboot to fallback image
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_1;
|
||||
tgtCopy = xsc::COPY_1;
|
||||
}
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_1;
|
||||
tgtCopy = xsc::COPY_0;
|
||||
}
|
||||
} else {
|
||||
// Reboot on fallback image
|
||||
}
|
||||
}
|
||||
if ((CURRENT_COPY == xsc::COPY_1) and (CURRENT_CHIP == xsc::CHIP_0) and
|
||||
(rf.img10Cnt >= rf.maxCount)) {
|
||||
needsReboot = true;
|
||||
if (rf.img11Cnt >= rf.maxCount) {
|
||||
if (rf.img00Cnt >= rf.maxCount) {
|
||||
if (rf.img01Cnt >= rf.maxCount) {
|
||||
// Reboot to fallback image
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_0;
|
||||
tgtCopy = xsc::COPY_1;
|
||||
}
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_0;
|
||||
tgtCopy = xsc::COPY_0;
|
||||
}
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_1;
|
||||
tgtCopy = xsc::COPY_1;
|
||||
}
|
||||
}
|
||||
if ((CURRENT_COPY == xsc::COPY_1) and (CURRENT_CHIP == xsc::CHIP_1) and
|
||||
(rf.img11Cnt >= rf.maxCount)) {
|
||||
needsReboot = true;
|
||||
if (rf.img10Cnt >= rf.maxCount) {
|
||||
if (rf.img00Cnt >= rf.maxCount) {
|
||||
if (rf.img01Cnt >= rf.maxCount) {
|
||||
// Reboot to fallback image
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_0;
|
||||
tgtCopy = xsc::COPY_1;
|
||||
}
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_0;
|
||||
tgtCopy = xsc::COPY_0;
|
||||
}
|
||||
} else {
|
||||
tgtChip = xsc::CHIP_1;
|
||||
tgtCopy = xsc::COPY_0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CoreController::parseRebootFile(std::string path, RebootFile &rf) {
|
||||
using namespace std;
|
||||
std::string selfMatch;
|
||||
if (CURRENT_CHIP == xsc::CHIP_0) {
|
||||
if (CURRENT_COPY == xsc::COPY_0) {
|
||||
selfMatch = "00";
|
||||
} else {
|
||||
selfMatch = "01";
|
||||
}
|
||||
} else {
|
||||
if (CURRENT_COPY == xsc::COPY_0) {
|
||||
selfMatch = "10";
|
||||
} else {
|
||||
selfMatch = "11";
|
||||
}
|
||||
}
|
||||
ifstream file(path);
|
||||
string word;
|
||||
string line;
|
||||
uint8_t lineIdx = 0;
|
||||
while (std::getline(file, line)) {
|
||||
istringstream iss(line);
|
||||
switch (lineIdx) {
|
||||
case 0: {
|
||||
iss >> word;
|
||||
if (word.find("on:") == string::npos) {
|
||||
// invalid file
|
||||
return false;
|
||||
}
|
||||
iss >> rf.enabled;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
iss >> word;
|
||||
if (word.find("maxcnt:") == string::npos) {
|
||||
return false;
|
||||
}
|
||||
iss >> rf.maxCount;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
iss >> word;
|
||||
if (word.find("img00:") == string::npos) {
|
||||
return false;
|
||||
}
|
||||
iss >> rf.img00Cnt;
|
||||
if (word.find(selfMatch) != string::npos) {
|
||||
rf.relevantBootCnt = rf.img00Cnt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
iss >> word;
|
||||
if (word.find("img01:") == string::npos) {
|
||||
return false;
|
||||
}
|
||||
iss >> rf.img01Cnt;
|
||||
if (word.find(selfMatch) != string::npos) {
|
||||
rf.relevantBootCnt = rf.img01Cnt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
iss >> word;
|
||||
if (word.find("img10:") == string::npos) {
|
||||
return false;
|
||||
}
|
||||
iss >> rf.img10Cnt;
|
||||
if (word.find(selfMatch) != string::npos) {
|
||||
rf.relevantBootCnt = rf.img10Cnt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
iss >> word;
|
||||
if (word.find("img11:") == string::npos) {
|
||||
return false;
|
||||
}
|
||||
iss >> rf.img11Cnt;
|
||||
if (word.find(selfMatch) != string::npos) {
|
||||
rf.relevantBootCnt = rf.img11Cnt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 6: {
|
||||
iss >> word;
|
||||
if (word.find("bootflag:") == string::npos) {
|
||||
return false;
|
||||
}
|
||||
iss >> rf.bootFlag;
|
||||
break;
|
||||
}
|
||||
case 7: {
|
||||
iss >> word;
|
||||
if (word.find("bootflag:") == string::npos) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
iss >> word;
|
||||
uint8_t copyRaw = 0;
|
||||
uint8_t chipRaw = 0;
|
||||
if (word.find("last:") == string::npos) {
|
||||
return false;
|
||||
}
|
||||
iss >> chipRaw;
|
||||
if (iss.fail()) {
|
||||
return false;
|
||||
}
|
||||
iss >> copyRaw;
|
||||
if (iss.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chipRaw > 1 or copyRaw > 1) {
|
||||
return false;
|
||||
}
|
||||
rf.lastChip = static_cast<xsc::Chip>(chipRaw);
|
||||
rf.lastCopy = static_cast<xsc::Copy>(copyRaw);
|
||||
}
|
||||
}
|
||||
if (iss.fail()) {
|
||||
return false;
|
||||
}
|
||||
lineIdx++;
|
||||
}
|
||||
if (lineIdx < 8) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CoreController::resetRebootCount(xsc::Chip tgtChip, xsc::Copy tgtCopy) {
|
||||
std::string path = sdcMan->getCurrentMountPrefix(sdInfo.pref) + REBOOT_FILE;
|
||||
// Disable the reboot file mechanism
|
||||
parseRebootFile(path, rebootFile);
|
||||
if (tgtChip == xsc::ALL_CHIP and tgtCopy == xsc::ALL_COPY) {
|
||||
rebootFile.img00Cnt = 0;
|
||||
rebootFile.img01Cnt = 0;
|
||||
rebootFile.img10Cnt = 0;
|
||||
rebootFile.img11Cnt = 0;
|
||||
} else {
|
||||
if (tgtChip == xsc::CHIP_0) {
|
||||
if (tgtCopy == xsc::COPY_0) {
|
||||
rebootFile.img00Cnt = 0;
|
||||
} else {
|
||||
rebootFile.img01Cnt = 0;
|
||||
}
|
||||
} else {
|
||||
if (tgtCopy == xsc::COPY_0) {
|
||||
rebootFile.img10Cnt = 0;
|
||||
} else {
|
||||
rebootFile.img11Cnt = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
rewriteRebootFile(rebootFile);
|
||||
}
|
||||
|
||||
void CoreController::rewriteRebootFile(RebootFile file) {
|
||||
std::string path = sdcMan->getCurrentMountPrefix(sdInfo.pref) + REBOOT_FILE;
|
||||
std::ofstream rebootFile(path);
|
||||
if (rebootFile.is_open()) {
|
||||
// Initiate reboot file first. Reboot handling will be on on initialization
|
||||
rebootFile << "on: " << file.enabled << "\nmaxcnt: " << file.maxCount
|
||||
<< "\nimg00: " << file.img00Cnt << "\nimg01: " << file.img01Cnt
|
||||
<< "\nimg10: " << file.img10Cnt << "\nimg11: " << file.img11Cnt
|
||||
<< "\nbootflag: " << file.bootFlag << "\nlast: " << file.lastChip << " "
|
||||
<< file.lastCopy << "\n";
|
||||
}
|
||||
}
|
59
unittest/rebootLogic/CoreController.h
Normal file
59
unittest/rebootLogic/CoreController.h
Normal file
@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "SdCardManager.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace xsc {
|
||||
|
||||
enum Chip : uint8_t { CHIP_0, CHIP_1, NO_CHIP, SELF_CHIP, ALL_CHIP };
|
||||
enum Copy : uint8_t { COPY_0, COPY_1, NO_COPY, SELF_COPY, ALL_COPY };
|
||||
|
||||
} // namespace xsc
|
||||
|
||||
|
||||
struct RebootFile {
|
||||
static constexpr uint8_t DEFAULT_MAX_BOOT_CNT = 10;
|
||||
|
||||
bool enabled = false;
|
||||
size_t maxCount = DEFAULT_MAX_BOOT_CNT;
|
||||
uint8_t img00Cnt = 0;
|
||||
uint8_t img01Cnt = 0;
|
||||
uint8_t img10Cnt = 0;
|
||||
uint8_t img11Cnt = 0;
|
||||
uint8_t relevantBootCnt = 0;
|
||||
bool bootFlag = false;
|
||||
xsc::Chip lastChip = xsc::Chip::CHIP_0;
|
||||
xsc::Copy lastCopy = xsc::Copy::COPY_0;
|
||||
};
|
||||
|
||||
class SdCardManager;
|
||||
|
||||
class CoreController {
|
||||
public:
|
||||
static constexpr char REBOOT_FILE[] = "/conf/reboot.txt";
|
||||
static constexpr uint32_t REBOOT_MECHANISM_TRIGGERED = 1;
|
||||
static xsc::Chip CURRENT_CHIP;
|
||||
static xsc::Copy CURRENT_COPY;
|
||||
|
||||
CoreController();
|
||||
void performRebootFileHandling(bool recreateFile);
|
||||
void determineAndExecuteReboot(RebootFile& rf, bool& needsReboot, xsc::Chip& tgtChip,
|
||||
xsc::Copy& tgtCopy);
|
||||
void resetRebootCount(xsc::Chip tgtChip, xsc::Copy tgtCopy);
|
||||
bool parseRebootFile(std::string path, RebootFile& file);
|
||||
void rewriteRebootFile(RebootFile file);
|
||||
private:
|
||||
struct SdInfo {
|
||||
sd::SdCard pref = sd::SdCard::NONE;
|
||||
sd::SdState prefState = sd::SdState::OFF;
|
||||
sd::SdCard other = sd::SdCard::NONE;
|
||||
sd::SdState otherState = sd::SdState::OFF;
|
||||
} sdInfo;
|
||||
|
||||
SdCardManager* sdcMan = nullptr;
|
||||
RebootFile rebootFile = {};
|
||||
bool doPerformRebootFileHandling = true;
|
||||
};
|
9
unittest/rebootLogic/SdCardManager.cpp
Normal file
9
unittest/rebootLogic/SdCardManager.cpp
Normal file
@ -0,0 +1,9 @@
|
||||
#include "SdCardManager.h"
|
||||
|
||||
std::string SdCardManager::getCurrentMountPrefix(sd::SdCard prefSdCard) {
|
||||
return "/tmp";
|
||||
}
|
||||
|
||||
bool SdCardManager::isSdCardMounted(sd::SdCard sdCard) {
|
||||
return true;
|
||||
}
|
23
unittest/rebootLogic/SdCardManager.h
Normal file
23
unittest/rebootLogic/SdCardManager.h
Normal file
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace sd {
|
||||
|
||||
enum SdState : uint8_t {
|
||||
OFF = 0,
|
||||
ON = 1,
|
||||
// A mounted SD card is on as well
|
||||
MOUNTED = 2
|
||||
};
|
||||
|
||||
enum SdCard : uint8_t { SLOT_0 = 0, SLOT_1 = 1, BOTH, NONE };
|
||||
|
||||
} // namespace sd
|
||||
|
||||
class SdCardManager {
|
||||
public:
|
||||
std::string getCurrentMountPrefix(sd::SdCard prefSdCard);
|
||||
bool isSdCardMounted(sd::SdCard sdCard);
|
||||
};
|
1
unittest/rebootLogic/conf.h
Normal file
1
unittest/rebootLogic/conf.h
Normal file
@ -0,0 +1 @@
|
||||
#define OBSW_VERBOSE_LEVEL 1
|
3
unittest/rebootLogic/event.cpp
Normal file
3
unittest/rebootLogic/event.cpp
Normal file
@ -0,0 +1,3 @@
|
||||
#include "event.h"
|
||||
|
||||
void triggerEvent(uint32_t event, uint32_t p1, uint32_t p2) {}
|
5
unittest/rebootLogic/event.h
Normal file
5
unittest/rebootLogic/event.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
void triggerEvent(uint32_t event, uint32_t p1, uint32_t p2);
|
5
unittest/rebootLogic/libxiphos.cpp
Normal file
5
unittest/rebootLogic/libxiphos.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
#include "libxiphos.h"
|
||||
|
||||
void xsc_boot_copy(xsc_libnor_chip_t boot_chip, xsc_libnor_copy_t boot_copy) {
|
||||
|
||||
}
|
17
unittest/rebootLogic/libxiphos.h
Normal file
17
unittest/rebootLogic/libxiphos.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
/** Type for identifying the nominal or the gold (redundant) flash copy */
|
||||
typedef enum {
|
||||
XSC_LIBNOR_COPY_NOMINAL,
|
||||
XSC_LIBNOR_COPY_GOLD,
|
||||
XSC_LIBNOR_COPY_TOTAL_NUMBER
|
||||
} xsc_libnor_copy_t;
|
||||
|
||||
/** Type for identifying on of the two NOR flash chips */
|
||||
typedef enum {
|
||||
XSC_LIBNOR_CHIP_0, /* First NOR flash chip */
|
||||
XSC_LIBNOR_CHIP_1, /* Second NOR flash chip */
|
||||
XSC_LIBNOR_CHIP_TOTAL_NUMBER
|
||||
} xsc_libnor_chip_t;
|
||||
|
||||
void xsc_boot_copy(xsc_libnor_chip_t boot_chip, xsc_libnor_copy_t boot_copy);
|
@ -1,5 +1,7 @@
|
||||
#include "CoreController.h"
|
||||
#include <iostream>
|
||||
|
||||
int main(int, char**) {
|
||||
CoreController ctrl;
|
||||
std::cout << "Hello, world!\n";
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user