/*----------------------------------------------------------------------------*/ | |
/* Copyright (c) FIRST 2011. All Rights Reserved. */ | |
/* Open Source Software - may be modified and shared by FRC teams. The code */ | |
/* must be accompanied by the FIRST BSD license file in $(WIND_BASE)/WPILib. */ | |
/*----------------------------------------------------------------------------*/ | |
#include "Preferences.h" | |
#include "NetworkCommunication/UsageReporting.h" | |
#include "Synchronized.h" | |
#include "WPIErrors.h" | |
#include <stdio.h> | |
#include <algorithm> | |
/** Private NI function needed to write to the VxWorks target */ | |
extern "C" int Priv_SetWriteFileAllowed(UINT32 enable); | |
/** The Preferences table name */ | |
static const char *kTableName = "Preferences"; | |
/** The value of the save field */ | |
static const char *kSaveField = "~S A V E~"; | |
/** The file to save to */ | |
static const char *kFileName = "/c/wpilib-preferences.ini"; | |
/** The characters to put between a field and value */ | |
static const char *kValuePrefix = "=\""; | |
/** The characters to put after the value */ | |
static const char *kValueSuffix = "\"\n"; | |
/** The singleton instance */ | |
Preferences *Preferences::_instance = NULL; | |
Preferences::Preferences() : | |
m_fileLock(NULL), | |
m_fileOpStarted(NULL), | |
m_tableLock(NULL), | |
m_readTask("PreferencesReadTask", (FUNCPTR)Preferences::InitReadTask), | |
m_writeTask("PreferencesWriteTask", (FUNCPTR)Preferences::InitWriteTask) | |
{ | |
m_fileLock = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE | SEM_DELETE_SAFE); | |
m_fileOpStarted = semBCreate (SEM_Q_PRIORITY, SEM_EMPTY); | |
m_tableLock = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE | SEM_DELETE_SAFE); | |
Synchronized sync(m_fileLock); | |
m_readTask.Start((UINT32)this); | |
semTake(m_fileOpStarted, WAIT_FOREVER); | |
nUsageReporting::report(nUsageReporting::kResourceType_Preferences, 0); | |
} | |
Preferences::~Preferences() | |
{ | |
semTake(m_tableLock, WAIT_FOREVER); | |
semDelete(m_tableLock); | |
semTake(m_fileLock, WAIT_FOREVER); | |
semDelete(m_fileOpStarted); | |
semDelete(m_fileLock); | |
} | |
/** | |
* Get the one and only {@link Preferences} object | |
* @return pointer to the {@link Preferences} | |
*/ | |
Preferences *Preferences::GetInstance() | |
{ | |
if (_instance == NULL) | |
_instance = new Preferences; | |
return _instance; | |
} | |
/** | |
* Returns a vector of all the keys | |
* @return a vector of the keys | |
*/ | |
std::vector<std::string> Preferences::GetKeys() | |
{ | |
return m_keys; | |
} | |
/** | |
* Returns the string at the given key. If this table does not have a value | |
* for that position, then the given defaultValue will be returned. | |
* @param key the key | |
* @param defaultValue the value to return if none exists in the table | |
* @return either the value in the table, or the defaultValue | |
*/ | |
std::string Preferences::GetString(const char *key, const char *defaultValue) | |
{ | |
std::string value = Get(key); | |
return value.empty() ? defaultValue : value; | |
} | |
/** | |
* Returns the string at the given key. If this table does not have a value | |
* for that position, then the given defaultValue will be returned. | |
* @param key the key | |
* @param value the buffer to copy the value into | |
* @param valueSize the size of value | |
* @param defaultValue the value to return if none exists in the table | |
* @return The size of the returned string | |
*/ | |
int Preferences::GetString(const char *key, char *value, int valueSize, const char *defaultValue) | |
{ | |
std::string stringValue = GetString(key, defaultValue); | |
stringValue.copy(value, valueSize); | |
return stringValue.size(); | |
} | |
/** | |
* Returns the int at the given key. If this table does not have a value | |
* for that position, then the given defaultValue value will be returned. | |
* @param key the key | |
* @param defaultValue the value to return if none exists in the table | |
* @return either the value in the table, or the defaultValue | |
*/ | |
int Preferences::GetInt(const char *key, int defaultValue) | |
{ | |
std::string value = Get(key); | |
if (value.empty()) | |
return defaultValue; | |
return strtol(value.c_str(), NULL, 0); | |
} | |
/** | |
* Returns the double at the given key. If this table does not have a value | |
* for that position, then the given defaultValue value will be returned. | |
* @param key the key | |
* @param defaultValue the value to return if none exists in the table | |
* @return either the value in the table, or the defaultValue | |
*/ | |
double Preferences::GetDouble(const char *key, double defaultValue) | |
{ | |
std::string value = Get(key); | |
if (value.empty()) | |
return defaultValue; | |
return strtod(value.c_str(), NULL); | |
} | |
/** | |
* Returns the float at the given key. If this table does not have a value | |
* for that position, then the given defaultValue value will be returned. | |
* @param key the key | |
* @param defaultValue the value to return if none exists in the table | |
* @return either the value in the table, or the defaultValue | |
*/ | |
float Preferences::GetFloat(const char *key, float defaultValue) | |
{ | |
std::string value = Get(key); | |
if (value.empty()) | |
return defaultValue; | |
return strtod(value.c_str(), NULL); | |
} | |
/** | |
* Returns the boolean at the given key. If this table does not have a value | |
* for that position, then the given defaultValue value will be returned. | |
* @param key the key | |
* @param defaultValue the value to return if none exists in the table | |
* @return either the value in the table, or the defaultValue | |
*/ | |
bool Preferences::GetBoolean(const char *key, bool defaultValue) | |
{ | |
std::string value = Get(key); | |
if (value.empty()) | |
return defaultValue; | |
if (value.compare("true") == 0) | |
return true; | |
else if (value.compare("false") == 0) | |
return false; | |
wpi_setWPIErrorWithContext(ParameterOutOfRange, "Boolean value does not contain \"true\" or \"false\""); | |
return false; | |
} | |
/** | |
* Returns the long (INT64) at the given key. If this table does not have a value | |
* for that position, then the given defaultValue value will be returned. | |
* @param key the key | |
* @param defaultValue the value to return if none exists in the table | |
* @return either the value in the table, or the defaultValue | |
*/ | |
INT64 Preferences::GetLong(const char *key, INT64 defaultValue) | |
{ | |
std::string value = Get(key); | |
if (value.empty()) | |
return defaultValue; | |
// Ummm... not available in our VxWorks... | |
//return strtoll(value.c_str(), NULL, 0); | |
INT64 intVal; | |
sscanf(value.c_str(), "%lld", &intVal); | |
return intVal; | |
} | |
/** | |
* Puts the given string into the preferences table. | |
* | |
* <p>The value may not have quotation marks, nor may the key | |
* have any whitespace nor an equals sign</p> | |
* | |
* <p>This will <b>NOT</b> save the value to memory between power cycles, | |
* to do that you must call {@link Preferences#Save() Save()} (which must be used with care). | |
* at some point after calling this.</p> | |
* @param key the key | |
* @param value the value | |
*/ | |
void Preferences::PutString(const char *key, const char *value) | |
{ | |
if (value == NULL) | |
{ | |
wpi_setWPIErrorWithContext(NullParameter, "value"); | |
return; | |
} | |
if (std::string(value).find_first_of("\"") != std::string::npos) | |
{ | |
wpi_setWPIErrorWithContext(ParameterOutOfRange, "value contains illegal characters"); | |
return; | |
} | |
Put(key, value); | |
} | |
/** | |
* Puts the given int into the preferences table. | |
* | |
* <p>The key may not have any whitespace nor an equals sign</p> | |
* | |
* <p>This will <b>NOT</b> save the value to memory between power cycles, | |
* to do that you must call {@link Preferences#Save() Save()} (which must be used with care) | |
* at some point after calling this.</p> | |
* @param key the key | |
* @param value the value | |
*/ | |
void Preferences::PutInt(const char *key, int value) | |
{ | |
char buf[32]; | |
snprintf(buf, 32, "%d", value); | |
Put(key, buf); | |
} | |
/** | |
* Puts the given double into the preferences table. | |
* | |
* <p>The key may not have any whitespace nor an equals sign</p> | |
* | |
* <p>This will <b>NOT</b> save the value to memory between power cycles, | |
* to do that you must call {@link Preferences#Save() Save()} (which must be used with care) | |
* at some point after calling this.</p> | |
* @param key the key | |
* @param value the value | |
*/ | |
void Preferences::PutDouble(const char *key, double value) | |
{ | |
char buf[32]; | |
snprintf(buf, 32, "%f", value); | |
Put(key, buf); | |
} | |
/** | |
* Puts the given float into the preferences table. | |
* | |
* <p>The key may not have any whitespace nor an equals sign</p> | |
* | |
* <p>This will <b>NOT</b> save the value to memory between power cycles, | |
* to do that you must call {@link Preferences#Save() Save()} (which must be used with care) | |
* at some point after calling this.</p> | |
* @param key the key | |
* @param value the value | |
*/ | |
void Preferences::PutFloat(const char *key, float value) | |
{ | |
char buf[32]; | |
snprintf(buf, 32, "%f", value); | |
Put(key, buf); | |
} | |
/** | |
* Puts the given boolean into the preferences table. | |
* | |
* <p>The key may not have any whitespace nor an equals sign</p> | |
* | |
* <p>This will <b>NOT</b> save the value to memory between power cycles, | |
* to do that you must call {@link Preferences#Save() Save()} (which must be used with care) | |
* at some point after calling this.</p> | |
* @param key the key | |
* @param value the value | |
*/ | |
void Preferences::PutBoolean(const char *key, bool value) | |
{ | |
Put(key, value ? "true" : "false"); | |
} | |
/** | |
* Puts the given long (INT64) into the preferences table. | |
* | |
* <p>The key may not have any whitespace nor an equals sign</p> | |
* | |
* <p>This will <b>NOT</b> save the value to memory between power cycles, | |
* to do that you must call {@link Preferences#Save() Save()} (which must be used with care) | |
* at some point after calling this.</p> | |
* @param key the key | |
* @param value the value | |
*/ | |
void Preferences::PutLong(const char *key, INT64 value) | |
{ | |
char buf[32]; | |
snprintf(buf, 32, "%lld", value); | |
Put(key, buf); | |
} | |
/** | |
* Saves the preferences to a file on the cRIO. | |
* | |
* <p>This should <b>NOT</b> be called often. | |
* Too many writes can damage the cRIO's flash memory. | |
* While it is ok to save once or twice a match, this should never | |
* be called every run of {@link IterativeRobot#TeleopPeriodic()}, etc.</p> | |
* | |
* <p>The actual writing of the file is done in a separate thread. | |
* However, any call to a get or put method will wait until the table is fully saved before continuing.</p> | |
*/ | |
void Preferences::Save() | |
{ | |
Synchronized sync(m_fileLock); | |
m_writeTask.Start((UINT32)this); | |
semTake(m_fileOpStarted, WAIT_FOREVER); | |
} | |
/** | |
* Returns whether or not there is a key with the given name. | |
* @param key the key | |
* @return if there is a value at the given key | |
*/ | |
bool Preferences::ContainsKey(const char *key) | |
{ | |
return !Get(key).empty(); | |
} | |
/** | |
* Remove a preference | |
* @param key the key | |
*/ | |
void Preferences::Remove(const char *key) | |
{ | |
m_values.erase(std::string(key)); | |
std::vector<std::string>::iterator it = m_keys.begin(); | |
for (; it != m_keys.end(); it++) | |
{ | |
if (it->compare(key) == 0) | |
{ | |
m_keys.erase(it); | |
break; | |
} | |
} | |
} | |
/** | |
* Returns the value at the given key. | |
* @param key the key | |
* @return the value (or empty if none exists) | |
*/ | |
std::string Preferences::Get(const char *key) | |
{ | |
Synchronized sync(m_tableLock); | |
if (key == NULL) | |
{ | |
wpi_setWPIErrorWithContext(NullParameter, "key"); | |
return std::string(""); | |
} | |
return m_values[std::string(key)]; | |
} | |
/** | |
* Puts the given value into the given key position | |
* @param key the key | |
* @param value the value | |
*/ | |
void Preferences::Put(const char *key, std::string value) | |
{ | |
Synchronized sync(m_tableLock); | |
if (key == NULL) | |
{ | |
wpi_setWPIErrorWithContext(NullParameter, "key"); | |
return; | |
} | |
if (std::string(key).find_first_of("=\n\r \t\"") != std::string::npos) | |
{ | |
wpi_setWPIErrorWithContext(ParameterOutOfRange, "key contains illegal characters"); | |
return; | |
} | |
std::pair<StringMap::iterator, bool> ret = | |
m_values.insert(StringMap::value_type(key, value)); | |
if (ret.second) | |
m_keys.push_back(key); | |
else | |
ret.first->second = value; | |
NetworkTable::GetTable(kTableName)->PutString(key, value); | |
} | |
/** | |
* The internal method to read from a file. | |
* This will be called in its own thread when the preferences singleton is | |
* first created. | |
*/ | |
void Preferences::ReadTaskRun() | |
{ | |
Synchronized sync(m_tableLock); | |
semGive(m_fileOpStarted); | |
std::string comment; | |
FILE *file = NULL; | |
file = fopen(kFileName, "r"); | |
if (file != NULL) | |
{ | |
std::string buffer; | |
while (true) | |
{ | |
char value; | |
do | |
{ | |
value = fgetc(file); | |
} while (value == ' ' || value == '\t'); | |
if (value == '\n' || value == ';') | |
{ | |
if (value == '\n') | |
{ | |
comment += "\n"; | |
} | |
else | |
{ | |
buffer.clear(); | |
for (; value != '\n' && !feof(file); value = fgetc(file)) | |
buffer += value; | |
buffer += '\n'; | |
comment += buffer; | |
} | |
} | |
else if (value == '[') | |
{ | |
// Find the end of the section and the new line after it and throw it away | |
for (; value != ']' && !feof(file); value = fgetc(file)); | |
for (; value != '\n' && !feof(file); value = fgetc(file)); | |
} | |
else | |
{ | |
buffer.clear(); | |
for (; value != '=' && !feof(file); ) | |
{ | |
buffer += value; | |
do | |
{ | |
value = fgetc(file); | |
} while (value == ' ' || value == '\t'); | |
} | |
std::string name = buffer; | |
buffer.clear(); | |
bool shouldBreak = false; | |
do | |
{ | |
value = fgetc(file); | |
} while (value == ' ' || value == '\t'); | |
if (value == '"') | |
{ | |
for (value = fgetc(file); value != '"' && !feof(file); value = fgetc(file)) | |
buffer += value; | |
// Clear the line | |
while (fgetc(file) != '\n' && !feof(file)); | |
} | |
else | |
{ | |
for (; value != '\n' && !feof(file);) | |
{ | |
buffer += value; | |
do | |
{ | |
value = fgetc(file); | |
} while (value == ' ' || value == '\t'); | |
} | |
if (feof(file)) | |
shouldBreak = true; | |
} | |
std::string value = buffer; | |
if (!name.empty() && !value.empty()) | |
{ | |
m_keys.push_back(name); | |
m_values.insert(std::pair<std::string, std::string>(name, value)); | |
NetworkTable::GetTable(kTableName)->PutString(name, value); | |
if (!comment.empty()) | |
{ | |
m_comments.insert(std::pair<std::string, std::string>(name, comment)); | |
comment.clear(); | |
} | |
} | |
if (shouldBreak) | |
break; | |
} | |
} | |
} | |
else | |
{ | |
wpi_setWPIErrorWithContext(NoAvailableResources, "Failed to open preferences file."); | |
} | |
if (file != NULL) | |
fclose(file); | |
if (!comment.empty()) | |
m_endComment = comment; | |
NetworkTable::GetTable(kTableName)->PutBoolean(kSaveField, false); | |
NetworkTable::GetTable(kTableName)->AddTableListener(this); | |
} | |
/** | |
* Internal method that actually writes the table to a file. | |
* This is called in its own thread when {@link Preferences#Save() Save()} is called. | |
*/ | |
void Preferences::WriteTaskRun() | |
{ | |
Synchronized sync(m_tableLock); | |
semGive(m_fileOpStarted); | |
FILE *file = NULL; | |
Priv_SetWriteFileAllowed(1); | |
file = fopen(kFileName, "w"); | |
fputs("[Preferences]\n", file); | |
std::vector<std::string>::iterator it = m_keys.begin(); | |
for (; it != m_keys.end(); it++) | |
{ | |
std::string key = *it; | |
std::string value = m_values[key]; | |
std::string comment = m_comments[key]; | |
if (!comment.empty()) | |
fputs(comment.c_str(), file); | |
fputs(key.c_str(), file); | |
fputs(kValuePrefix, file); | |
fputs(value.c_str(), file); | |
fputs(kValueSuffix, file); | |
} | |
if (!m_endComment.empty()) | |
fputs(m_endComment.c_str(), file); | |
if (file != NULL) | |
fclose(file); | |
NetworkTable::GetTable(kTableName)->PutBoolean(kSaveField, false); | |
} | |
static bool isKeyAcceptable(const std::string& value) { | |
for (unsigned int i = 0; i < value.length(); i++) { | |
char letter = value.at(i); | |
switch (letter) { | |
case '=': | |
case '\n': | |
case '\r': | |
case ' ': | |
case '\t': | |
return false; | |
} | |
} | |
return true; | |
} | |
void Preferences::ValueChanged(ITable* table, const std::string& key, EntryValue value, bool isNew) | |
{ | |
if (key==kSaveField) | |
{ | |
if (table->GetBoolean(kSaveField, false)) | |
Save(); | |
} | |
else | |
{ | |
Synchronized sync(m_tableLock); | |
if (!isKeyAcceptable(key) || table->GetString(key, "").find('"')!=std::string::npos) | |
{ | |
if(m_values.find(key) != m_values.end()){ | |
m_values.erase(key); | |
std::vector<std::string>::iterator it = m_keys.begin(); | |
for (; it != m_keys.end(); it++) | |
{ | |
if (key==*it) | |
{ | |
m_keys.erase(it); | |
break; | |
} | |
} | |
table->PutString(key, "\""); | |
} | |
} | |
else | |
{ | |
std::pair<StringMap::iterator, bool> ret = | |
m_values.insert(StringMap::value_type(key, table->GetString(key, ""))); | |
if (ret.second) | |
m_keys.push_back(key); | |
else | |
ret.first->second = table->GetString(key, ""); | |
} | |
} | |
} |