mirror of
https://github.com/akuker/RASCSI.git
synced 2026-04-26 06:18:10 +00:00
Rebrand project to PiSCSI (#1016)
* Rebrand project to PiSCSI - rascsi ->piscsi - rasctl -> scsictl - rasdump -> scsidump - ras* -> piscsi* (rasutil -> piscsi_util, etc.) * Refined the formatting and wording of the app startup banner * Kept some references to rascsi and rasctl where backwards compatibility is concerned * Point to the new github repo URL Co-authored-by: nucleogenic <nr@nucleogenic.com> Co-authored-by: Uwe Seimet <Uwe.Seimet@seimet.de>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#include "shared/log.h"
|
||||
#include "generated/piscsi_interface.pb.h"
|
||||
#include "command_context.h"
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace piscsi_interface;
|
||||
|
||||
void CommandContext::Cleanup()
|
||||
{
|
||||
if (fd != -1) {
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool CommandContext::ReturnLocalizedError(LocalizationKey key, const string& arg1, const string& arg2,
|
||||
const string& arg3) const
|
||||
{
|
||||
return ReturnLocalizedError(key, NO_ERROR_CODE, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
bool CommandContext::ReturnLocalizedError(LocalizationKey key, PbErrorCode error_code, const string& arg1,
|
||||
const string& arg2, const string& arg3) const
|
||||
{
|
||||
// For the logfile always use English
|
||||
LOGERROR("%s", localizer.Localize(key, "en", arg1, arg2, arg3).c_str())
|
||||
|
||||
return ReturnStatus(false, localizer.Localize(key, locale, arg1, arg2, arg3), error_code, false);
|
||||
}
|
||||
|
||||
bool CommandContext::ReturnStatus(bool status, const string& msg, PbErrorCode error_code, bool log) const
|
||||
{
|
||||
// Do not log twice if logging has already been done in the localized error handling above
|
||||
if (log && !status && !msg.empty()) {
|
||||
LOGERROR("%s", msg.c_str())
|
||||
}
|
||||
|
||||
if (fd == -1) {
|
||||
if (!msg.empty()) {
|
||||
if (status) {
|
||||
cerr << "Error: " << msg << endl;
|
||||
}
|
||||
else {
|
||||
cout << msg << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
PbResult result;
|
||||
result.set_status(status);
|
||||
result.set_error_code(error_code);
|
||||
result.set_msg(msg);
|
||||
serializer.SerializeMessage(fd, result);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "generated/piscsi_interface.pb.h"
|
||||
#include "localizer.h"
|
||||
#include "shared/protobuf_serializer.h"
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
using namespace piscsi_interface;
|
||||
|
||||
class CommandContext
|
||||
{
|
||||
const ProtobufSerializer serializer;
|
||||
|
||||
const Localizer localizer;
|
||||
|
||||
string locale;
|
||||
|
||||
int fd;
|
||||
|
||||
public:
|
||||
|
||||
CommandContext(const std::string& s = "", int f = -1) : locale(s), fd(f) {}
|
||||
~CommandContext() = default;
|
||||
|
||||
void Cleanup();
|
||||
|
||||
const ProtobufSerializer& GetSerializer() const { return serializer; }
|
||||
int GetFd() const { return fd; }
|
||||
void SetFd(int f) { fd = f; }
|
||||
bool IsValid() const { return fd != -1; }
|
||||
|
||||
bool ReturnLocalizedError(LocalizationKey, const string& = "", const string& = "", const string& = "") const;
|
||||
bool ReturnLocalizedError(LocalizationKey, PbErrorCode, const string& = "", const string& = "", const string& = "") const;
|
||||
bool ReturnStatus(bool = true, const string& = "", PbErrorCode = PbErrorCode::NO_ERROR_CODE, bool = true) const;
|
||||
};
|
||||
@@ -0,0 +1,213 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#include "localizer.h"
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <regex>
|
||||
|
||||
using namespace std;
|
||||
|
||||
Localizer::Localizer()
|
||||
{
|
||||
Add(LocalizationKey::ERROR_AUTHENTICATION, "en", "Authentication failed");
|
||||
Add(LocalizationKey::ERROR_AUTHENTICATION, "de", "Authentifizierung fehlgeschlagen");
|
||||
Add(LocalizationKey::ERROR_AUTHENTICATION, "sv", "Autentiseringen misslyckades");
|
||||
Add(LocalizationKey::ERROR_AUTHENTICATION, "fr", "Authentification éronnée");
|
||||
Add(LocalizationKey::ERROR_AUTHENTICATION, "es", "Fallo de autentificación");
|
||||
Add(LocalizationKey::ERROR_OPERATION, "en", "Unknown operation");
|
||||
Add(LocalizationKey::ERROR_OPERATION, "de", "Unbekannte Operation");
|
||||
Add(LocalizationKey::ERROR_OPERATION, "sv", "Okänd operation");
|
||||
Add(LocalizationKey::ERROR_OPERATION, "fr", "Opération inconnue");
|
||||
Add(LocalizationKey::ERROR_OPERATION, "es", "Operación desconocida");
|
||||
Add(LocalizationKey::ERROR_LOG_LEVEL, "en", "Invalid log level %1");
|
||||
Add(LocalizationKey::ERROR_LOG_LEVEL, "de", "Ungültiger Log-Level %1");
|
||||
Add(LocalizationKey::ERROR_LOG_LEVEL, "sv", "Ogiltig loggnivå %1");
|
||||
Add(LocalizationKey::ERROR_LOG_LEVEL, "fr", "Niveau de journalisation invalide %1");
|
||||
Add(LocalizationKey::ERROR_LOG_LEVEL, "es", "Nivel de registro %1 no válido");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "en", "Missing device ID");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "de", "Fehlende Geräte-ID");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "sv", "Enhetens id saknas");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "fr", "ID de périphérique manquante");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "es", "Falta el ID del dispositivo");
|
||||
Add(LocalizationKey::ERROR_MISSING_FILENAME, "en", "Missing filename");
|
||||
Add(LocalizationKey::ERROR_MISSING_FILENAME, "de", "Fehlender Dateiname");
|
||||
Add(LocalizationKey::ERROR_MISSING_FILENAME, "sv", "Filnamn saknas");
|
||||
Add(LocalizationKey::ERROR_MISSING_FILENAME, "fr", "Nom de fichier manquant");
|
||||
Add(LocalizationKey::ERROR_MISSING_FILENAME, "es", "Falta el nombre del archivo");
|
||||
Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "en", "Device type %1 requires a filename");
|
||||
Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "de", "Gerätetyp %1 erfordert einen Dateinamen");
|
||||
Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "sv", "Enhetstypen %1 kräver ett filnamn");
|
||||
Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "es", "El tipo de dispositivo %1 requiere un nombre de archivo");
|
||||
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "en", "Image file '%1' is already being used by ID %2, unit %3");
|
||||
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "de", "Image-Datei '%1' wird bereits von ID %2, Einheit %3 benutzt");
|
||||
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "sv", "Skivbildsfilen '%1' används redan av id %2, enhetsnummer %3");
|
||||
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "fr", "Le fichier d'image '%1' est déjà utilisé par l'ID %2, unité %3");
|
||||
Add(LocalizationKey::ERROR_IMAGE_IN_USE, "es", "El archivo de imagen '%1' ya está siendo utilizado por el ID %2, unidad %3");
|
||||
Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "en", "Can't create image file info for '%1'");
|
||||
Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "de", "Image-Datei-Information für '%1' kann nicht erzeugt werden");
|
||||
Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "sv", "Kunde ej skapa skivbildsfilsinfo för '%1'");
|
||||
Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "es", "No se puede crear información de archivo de imagen para '%1'");
|
||||
Add(LocalizationKey::ERROR_RESERVED_ID, "en", "Device ID %1 is reserved");
|
||||
Add(LocalizationKey::ERROR_RESERVED_ID, "de", "Geräte-ID %1 ist reserviert");
|
||||
Add(LocalizationKey::ERROR_RESERVED_ID, "sv", "Enhets-id %1 är reserverat");
|
||||
Add(LocalizationKey::ERROR_RESERVED_ID, "fr", "ID de périphérique %1 réservée");
|
||||
Add(LocalizationKey::ERROR_RESERVED_ID, "es", "El ID de dispositivo %1 está reservado");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "en", "Command for non-existing ID %1");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "de", "Kommando für nicht existente ID %1");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "sv", "Kommando för id %1 som ej existerar");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "fr", "Commande pour ID %1 non-existant");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "es", "Comando para ID %1 no existente");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "en", "Command for non-existing ID %1, unit %2");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "de", "Kommando für nicht existente ID %1, Einheit %2");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "sv", "Kommando för id %1, enhetsnummer %2 som ej existerar");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "fr", "Command pour ID %1, unité %2 non-existant");
|
||||
Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "es", "Comando para ID %1 inexistente, unidad %2");
|
||||
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "en", "Unknown device type %1");
|
||||
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "de", "Unbekannter Gerätetyp %1");
|
||||
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "sv", "Obekant enhetstyp: %1");
|
||||
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "fr", "Type de périphérique inconnu %1");
|
||||
Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "es", "Tipo de dispositivo desconocido %1");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "en", "Device type required for unknown extension of file '%1'");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "de", "Gerätetyp erforderlich für unbekannte Extension der Datei '%1'");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "sv", "Man måste ange enhetstyp för obekant filändelse '%1'");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "fr", "Type de périphérique requis pour extension inconnue du fichier '%1'");
|
||||
Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "es", "Tipo de dispositivo requerido para la extensión desconocida del archivo '%1'");
|
||||
Add(LocalizationKey::ERROR_DUPLICATE_ID, "en", "Duplicate ID %1, unit %2");
|
||||
Add(LocalizationKey::ERROR_DUPLICATE_ID, "de", "Doppelte ID %1, Einheit %2");
|
||||
Add(LocalizationKey::ERROR_DUPLICATE_ID, "sv", "Duplikat id %1, enhetsnummer %2");
|
||||
Add(LocalizationKey::ERROR_DUPLICATE_ID, "fr", "ID %1, unité %2 dupliquée");
|
||||
Add(LocalizationKey::ERROR_DUPLICATE_ID, "es", "ID duplicado %1, unidad %2");
|
||||
Add(LocalizationKey::ERROR_DETACH, "en", "Couldn't detach device");
|
||||
Add(LocalizationKey::ERROR_DETACH, "de", "Geräte konnte nicht entfernt werden");
|
||||
Add(LocalizationKey::ERROR_DETACH, "sv", "Kunde ej koppla ifrån enheten");
|
||||
Add(LocalizationKey::ERROR_DETACH, "es", "No se ha podido desconectar el dispositivo");
|
||||
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "en", "Existing medium must first be ejected");
|
||||
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "de", "Das vorhandene Medium muss erst ausgeworfen werden");
|
||||
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "sv", "Nuvarande skiva måste utmatas först");
|
||||
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "fr", "Media déjà existant doit d'abord être éjecté");
|
||||
Add(LocalizationKey::ERROR_EJECT_REQUIRED, "es", "El medio existente debe ser expulsado primero");
|
||||
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "en", "Once set the device name cannot be changed anymore");
|
||||
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "de", "Ein bereits gesetzter Gerätename kann nicht mehr geändert werden");
|
||||
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "sv", "Enhetsnamn kan ej ändras efter att ha fastställts en gång");
|
||||
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "fr", "Une fois défini, le nom de périphérique ne peut plus être changé");
|
||||
Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "es", "Una vez establecido el nombre del dispositivo ya no se puede cambiar");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "en", "Missing shutdown mode");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "de", "Fehlender Shutdown-Modus");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "sv", "Avstängningsläge saknas");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "fr", "Mode d'extinction manquant");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "es", "Falta el modo de apagado");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "en", "Invalid shutdown mode '%1'");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "de", "Ungültiger Shutdown-Modus '%1'");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "sv", "Ogiltigt avstängsningsläge: '%1'");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "fr", "Mode d'extinction invalide '%1'");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "es", "Modo de apagado inválido '%1'");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "en", "Missing root permission for shutdown or reboot");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "de", "Fehlende Root-Berechtigung für Shutdown oder Neustart");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "sv", "Saknar root-rättigheter för att kunna stänga av eller starta om systemet");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "fr", "Permissions root manquantes pour extinction ou redémarrage");
|
||||
Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "es", "Falta el permiso de root para el apagado o el reinicio");
|
||||
Add(LocalizationKey::ERROR_FILE_OPEN, "en", "Invalid or non-existing file '%1'");
|
||||
Add(LocalizationKey::ERROR_FILE_OPEN, "de", "Ungültige oder fehlende Datei '%1'");
|
||||
Add(LocalizationKey::ERROR_FILE_OPEN, "sv", "Ogiltig eller saknad fil '%1'");
|
||||
Add(LocalizationKey::ERROR_FILE_OPEN, "fr", "Fichier invalide ou non-existant '%1'");
|
||||
Add(LocalizationKey::ERROR_FILE_OPEN, "es", "Archivo inválido o inexistente '%1'");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE, "en", "Invalid block size %1 bytes");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE, "de", "Ungültige Blockgröße %1 Bytes");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE, "sv", "Ogiltig blockstorlek: %1 byte");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE, "fr", "Taille de bloc invalide %1 octets");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE, "es", "Tamaño de bloque inválido %1 bytes");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "en", "Block size for device type %1 is not configurable");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "de", "Blockgröße für Gerätetyp %1 ist nicht konfigurierbar");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "sv", "Enhetstypen %1 kan inte använda andra blockstorlekar");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "fr", "Taille de block pour le type de périphérique %1 non configurable");
|
||||
Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "es", "El tamaño del bloque para el tipo de dispositivo %1 no es configurable");
|
||||
Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "en", "Couldn't create SCSI controller");
|
||||
Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "de", "SCSI-Controller konnte nicht erzeugt werden");
|
||||
Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "sv", "Kunde ej skapa SCSI-gränssnitt");
|
||||
Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "es", "No se ha podido crear el controlador SCSI");
|
||||
Add(LocalizationKey::ERROR_INVALID_ID, "en", "Invalid device ID %1 (0-%2)");
|
||||
Add(LocalizationKey::ERROR_INVALID_ID, "de", "Ungültige Geräte-ID %1 (0-%2)");
|
||||
Add(LocalizationKey::ERROR_INVALID_ID, "sv", "Ogiltigt enhets-id %1 (0-%2)");
|
||||
Add(LocalizationKey::ERROR_INVALID_ID, "es", "ID de dispositivo inválido %1 (0-%2)");
|
||||
Add(LocalizationKey::ERROR_INVALID_LUN, "en", "Invalid LUN %1 (0-%2)");
|
||||
Add(LocalizationKey::ERROR_INVALID_LUN, "de", "Ungültige LUN %1 (0-%2)");
|
||||
Add(LocalizationKey::ERROR_INVALID_LUN, "sv", "Ogiltigt enhetsnummer %1 (0-%2)");
|
||||
Add(LocalizationKey::ERROR_INVALID_LUN, "es", "LUN invalido %1 (0-%2)");
|
||||
Add(LocalizationKey::ERROR_LUN0, "en", "LUN 0 cannot be detached as long as there is still another LUN");
|
||||
Add(LocalizationKey::ERROR_LUN0, "de", "LUN 0 kann nicht entfernt werden, solange noch eine andere LUN existiert");
|
||||
Add(LocalizationKey::ERROR_LUN0, "sv", "Enhetsnummer 0 kan ej bli frånkopplat så länge som andra enhetsnummer är anslutna");
|
||||
Add(LocalizationKey::ERROR_LUN0, "es", "El LUN 0 no se puede desconectar mientras haya otro LUN");
|
||||
Add(LocalizationKey::ERROR_INITIALIZATION, "en", "Initialization of %1 device, ID %2, LUN %3 failed");
|
||||
Add(LocalizationKey::ERROR_INITIALIZATION, "de", "Initialisierung von %1-Gerät, ID %2, LUN %3 fehlgeschlagen");
|
||||
Add(LocalizationKey::ERROR_INITIALIZATION, "sv", "Kunde ej initialisera enheten %1 med id %2 och enhetsnummer %3");
|
||||
Add(LocalizationKey::ERROR_INITIALIZATION, "es", "La inicialización del dispositivo %1, ID %2, LUN %3 falló");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "en", "%1 operation denied, %2 isn't stoppable");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "de", "%1-Operation verweigert, %2 ist nicht stopbar");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "sv", "Operationen %1 nekades för att %2 inte kan stoppas");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "es", "%1 operación denegada, %2 no se puede parar");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "en", "%1 operation denied, %2 isn't removable");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "de", "%1-Operation verweigert, %2 ist nicht wechselbar");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "sv", "Operationen %1 nekades för att %2 inte är uttagbar(t)");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "es", "%1 operación denegada, %2 no es removible");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "en", "%1 operation denied, %2 isn't protectable");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "de", "%1-Operation verweigert, %2 ist nicht schützbar");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "sv", "Operationen %1 nekades för att %2 inte är skyddbar(t)");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "es", "%1 operación denegada, %2 no es protegible");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "en", "%1 operation denied, %2 isn't ready");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "de", "%1-Operation verweigert, %2 ist nicht bereit");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "sv", "Operationen %1 nekades för att %2 inte är redo");
|
||||
Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "es", "%1 operación denegada, %2 no está listo");
|
||||
}
|
||||
|
||||
void Localizer::Add(LocalizationKey key, const string& locale, string_view value)
|
||||
{
|
||||
// Safeguards against empty messages, duplicate entries and unsupported locales
|
||||
assert(locale.size());
|
||||
assert(value.size());
|
||||
assert(supported_languages.find(locale) != supported_languages.end());
|
||||
assert(localized_messages[locale][key].empty());
|
||||
|
||||
localized_messages[locale][key] = value;
|
||||
}
|
||||
|
||||
string Localizer::Localize(LocalizationKey key, const string& locale, const string& arg1, const string& arg2,
|
||||
const string &arg3) const
|
||||
{
|
||||
string locale_lower = locale;
|
||||
transform(locale_lower.begin(), locale_lower.end(), locale_lower.begin(), ::tolower);
|
||||
|
||||
auto it = localized_messages.find(locale_lower);
|
||||
if (it == localized_messages.end()) {
|
||||
// Try to fall back to country-indepedent locale (e.g. "en" instead of "en_US")
|
||||
if (locale_lower.length() > 2) {
|
||||
it = localized_messages.find(locale_lower.substr(0, 2));
|
||||
}
|
||||
if (it == localized_messages.end()) {
|
||||
it = localized_messages.find("en");
|
||||
}
|
||||
}
|
||||
|
||||
assert (it != localized_messages.end());
|
||||
|
||||
auto messages = it->second;
|
||||
|
||||
const auto& m = messages.find(key);
|
||||
if (m == messages.end()) {
|
||||
return "Missing localization for enum value " + to_string(static_cast<int>(key));
|
||||
}
|
||||
|
||||
string message = m->second;
|
||||
message = regex_replace(message, regex("%1"), arg1);
|
||||
message = regex_replace(message, regex("%2"), arg2);
|
||||
message = regex_replace(message, regex("%3"), arg3);
|
||||
|
||||
return message;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
// Message localization support. Currently only for messages with up to 3 string parameters.
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
|
||||
enum class LocalizationKey {
|
||||
ERROR_AUTHENTICATION,
|
||||
ERROR_OPERATION,
|
||||
ERROR_LOG_LEVEL,
|
||||
ERROR_MISSING_DEVICE_ID,
|
||||
ERROR_MISSING_FILENAME,
|
||||
ERROR_DEVICE_MISSING_FILENAME,
|
||||
ERROR_IMAGE_IN_USE,
|
||||
ERROR_IMAGE_FILE_INFO,
|
||||
ERROR_RESERVED_ID,
|
||||
ERROR_NON_EXISTING_DEVICE,
|
||||
ERROR_NON_EXISTING_UNIT,
|
||||
ERROR_UNKNOWN_DEVICE_TYPE,
|
||||
ERROR_MISSING_DEVICE_TYPE,
|
||||
ERROR_DUPLICATE_ID,
|
||||
ERROR_DETACH,
|
||||
ERROR_EJECT_REQUIRED,
|
||||
ERROR_DEVICE_NAME_UPDATE,
|
||||
ERROR_SHUTDOWN_MODE_MISSING,
|
||||
ERROR_SHUTDOWN_MODE_INVALID,
|
||||
ERROR_SHUTDOWN_PERMISSION,
|
||||
ERROR_FILE_OPEN,
|
||||
ERROR_BLOCK_SIZE,
|
||||
ERROR_BLOCK_SIZE_NOT_CONFIGURABLE,
|
||||
ERROR_SCSI_CONTROLLER,
|
||||
ERROR_INVALID_ID,
|
||||
ERROR_INVALID_LUN,
|
||||
ERROR_LUN0,
|
||||
ERROR_INITIALIZATION,
|
||||
ERROR_OPERATION_DENIED_STOPPABLE,
|
||||
ERROR_OPERATION_DENIED_REMOVABLE,
|
||||
ERROR_OPERATION_DENIED_PROTECTABLE,
|
||||
ERROR_OPERATION_DENIED_READY
|
||||
};
|
||||
|
||||
class Localizer
|
||||
{
|
||||
public:
|
||||
|
||||
Localizer();
|
||||
~Localizer() = default;
|
||||
|
||||
string Localize(LocalizationKey, const string&, const string& = "", const string& = "", const string& = "") const;
|
||||
|
||||
private:
|
||||
|
||||
void Add(LocalizationKey, const string&, string_view);
|
||||
unordered_map<string, unordered_map<LocalizationKey, string>> localized_messages;
|
||||
|
||||
// Supported locales, always lower case
|
||||
unordered_set<string> supported_languages = { "en", "de", "sv", "fr", "es" };
|
||||
};
|
||||
@@ -0,0 +1,661 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Powered by XM6 TypeG Technology.
|
||||
// Copyright (C) 2016-2020 GIMONS
|
||||
// Copyright (C) 2020-2022 Contributors to the PiSCSI project
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#include "shared/config.h"
|
||||
#include "shared/log.h"
|
||||
#include "shared/piscsi_util.h"
|
||||
#include "shared/protobuf_serializer.h"
|
||||
#include "shared/protobuf_util.h"
|
||||
#include "shared/piscsi_exceptions.h"
|
||||
#include "shared/piscsi_version.h"
|
||||
#include "controllers/controller_manager.h"
|
||||
#include "controllers/scsi_controller.h"
|
||||
#include "devices/device_factory.h"
|
||||
#include "devices/storage_device.h"
|
||||
#include "hal/gpiobus_factory.h"
|
||||
#include "hal/gpiobus.h"
|
||||
#include "hal/systimer.h"
|
||||
#include "piscsi/piscsi_executor.h"
|
||||
#include "piscsi/piscsi_core.h"
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
#include <netinet/in.h>
|
||||
#include <csignal>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <list>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace std;
|
||||
using namespace filesystem;
|
||||
using namespace spdlog;
|
||||
using namespace piscsi_interface;
|
||||
using namespace piscsi_util;
|
||||
using namespace protobuf_util;
|
||||
using namespace scsi_defs;
|
||||
|
||||
void Piscsi::Banner(const vector<char *>& args) const
|
||||
{
|
||||
cout << piscsi_util::Banner("(Backend Service)");
|
||||
cout << "Connection type: " << CONNECT_DESC << '\n' << flush;
|
||||
|
||||
if ((args.size() > 1 && strcmp(args[1], "-h") == 0) || (args.size() > 1 && strcmp(args[1], "--help") == 0)){
|
||||
cout << "\nUsage: " << args[0] << " [-idID[:LUN] FILE] ...\n\n";
|
||||
cout << " ID is SCSI device ID (0-7).\n";
|
||||
cout << " LUN is the optional logical unit (0-31).\n";
|
||||
cout << " FILE is a disk image file, \"daynaport\", \"bridge\", \"printer\" or \"services\".\n\n";
|
||||
cout << " Image type is detected based on file extension if no explicit type is specified.\n";
|
||||
cout << " hd1 : SCSI-1 HD image (Non-removable generic SCSI-1 HD image)\n";
|
||||
cout << " hds : SCSI HD image (Non-removable generic SCSI HD image)\n";
|
||||
cout << " hdr : SCSI HD image (Removable generic HD image)\n";
|
||||
cout << " hda : SCSI HD image (Apple compatible image)\n";
|
||||
cout << " hdn : SCSI HD image (NEC compatible image)\n";
|
||||
cout << " hdi : SCSI HD image (Anex86 HD image)\n";
|
||||
cout << " nhd : SCSI HD image (T98Next HD image)\n";
|
||||
cout << " mos : SCSI MO image (MO image)\n";
|
||||
cout << " iso : SCSI CD image (ISO 9660 image)\n" << flush;
|
||||
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
bool Piscsi::InitBus() const
|
||||
{
|
||||
bus = GPIOBUS_Factory::Create(BUS::mode_e::TARGET);
|
||||
if (bus == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto b = bus;
|
||||
controller_manager = make_shared<ControllerManager>(*b);
|
||||
auto c = controller_manager;
|
||||
executor = make_shared<PiscsiExecutor>(piscsi_image, *c);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Piscsi::Cleanup()
|
||||
{
|
||||
executor->DetachAll();
|
||||
|
||||
service.Cleanup();
|
||||
|
||||
bus->Cleanup();
|
||||
}
|
||||
|
||||
void Piscsi::ReadAccessToken(const string& filename) const
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(filename.c_str(), &st) || !S_ISREG(st.st_mode)) {
|
||||
throw parser_exception("Can't access token file '" + filename + "'");
|
||||
}
|
||||
|
||||
if (st.st_uid || st.st_gid) {
|
||||
throw parser_exception("Access token file '" + filename + "' must be owned by root");
|
||||
}
|
||||
|
||||
if (const auto perms = filesystem::status(filename).permissions();
|
||||
(perms & perms::group_read) != perms::none || (perms & perms::others_read) != perms::none ||
|
||||
(perms & perms::group_write) != perms::none || (perms & perms::others_write) != perms::none) {
|
||||
throw parser_exception("Access token file '" + filename + "' must be readable by root only");
|
||||
}
|
||||
|
||||
ifstream token_file(filename);
|
||||
if (token_file.fail()) {
|
||||
throw parser_exception("Can't open access token file '" + filename + "'");
|
||||
}
|
||||
|
||||
getline(token_file, access_token);
|
||||
if (token_file.fail()) {
|
||||
throw parser_exception("Can't read access token file '" + filename + "'");
|
||||
}
|
||||
|
||||
if (access_token.empty()) {
|
||||
throw parser_exception("Access token file '" + filename + "' must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
void Piscsi::LogDevices(string_view devices) const
|
||||
{
|
||||
stringstream ss(devices.data());
|
||||
string line;
|
||||
|
||||
while (getline(ss, line, '\n')) {
|
||||
LOGINFO("%s", line.c_str())
|
||||
}
|
||||
}
|
||||
|
||||
PbDeviceType Piscsi::ParseDeviceType(const string& value) const
|
||||
{
|
||||
string t = value;
|
||||
PbDeviceType type;
|
||||
transform(t.begin(), t.end(), t.begin(), ::toupper);
|
||||
if (!PbDeviceType_Parse(t, &type)) {
|
||||
throw parser_exception("Illegal device type '" + value + "'");
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
void Piscsi::TerminationHandler(int signum)
|
||||
{
|
||||
Cleanup();
|
||||
|
||||
exit(signum);
|
||||
}
|
||||
|
||||
Piscsi::optargs_type Piscsi::ParseArguments(const vector<char *>& args, int& port) const
|
||||
{
|
||||
optargs_type optargs;
|
||||
int block_size = 0;
|
||||
string name;
|
||||
|
||||
opterr = 1;
|
||||
int opt;
|
||||
while ((opt = getopt(static_cast<int>(args.size()), args.data(), "-Iib:d:n:p:r:t:z:D:F:L:P:R:C:v")) != -1) {
|
||||
switch (opt) {
|
||||
// The following options can not be processed until AFTER
|
||||
// the 'bus' object is created and configured
|
||||
case 'i':
|
||||
case 'I':
|
||||
case 'd':
|
||||
case 'D':
|
||||
case 'R':
|
||||
case 'n':
|
||||
case 'r':
|
||||
case 't':
|
||||
case 'F':
|
||||
case 'z':
|
||||
{
|
||||
const string optarg_str = optarg == nullptr ? "" : optarg;
|
||||
optargs.emplace_back(opt, optarg_str);
|
||||
continue;
|
||||
}
|
||||
|
||||
case 'b': {
|
||||
if (!GetAsUnsignedInt(optarg, block_size)) {
|
||||
throw parser_exception("Invalid block size " + string(optarg));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
case 'L':
|
||||
current_log_level = optarg;
|
||||
continue;
|
||||
|
||||
case 'p':
|
||||
if (!GetAsUnsignedInt(optarg, port) || port <= 0 || port > 65535) {
|
||||
throw parser_exception("Invalid port " + string(optarg) + ", port must be between 1 and 65535");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'P':
|
||||
ReadAccessToken(optarg);
|
||||
continue;
|
||||
|
||||
case 'v':
|
||||
cout << piscsi_get_version_string() << endl;
|
||||
exit(0);
|
||||
|
||||
case 1:
|
||||
{
|
||||
// Encountered filename
|
||||
const string optarg_str = (optarg == nullptr) ? "" : string(optarg);
|
||||
optargs.emplace_back(opt, optarg_str);
|
||||
continue;
|
||||
}
|
||||
|
||||
default:
|
||||
throw parser_exception("Parser error");
|
||||
}
|
||||
|
||||
if (optopt) {
|
||||
throw parser_exception("Praser error");
|
||||
}
|
||||
}
|
||||
|
||||
return optargs;
|
||||
}
|
||||
|
||||
void Piscsi::CreateInitialDevices(const optargs_type& optargs) const
|
||||
{
|
||||
PbCommand command;
|
||||
PbDeviceType type = UNDEFINED;
|
||||
int block_size = 0;
|
||||
string name;
|
||||
string log_level;
|
||||
string id_and_lun;
|
||||
|
||||
const char *locale = setlocale(LC_MESSAGES, "");
|
||||
if (locale == nullptr || !strcmp(locale, "C")) {
|
||||
locale = "en";
|
||||
}
|
||||
|
||||
opterr = 1;
|
||||
for (const auto& [option, value] : optargs) {
|
||||
switch (option) {
|
||||
case 'i':
|
||||
case 'I':
|
||||
continue;
|
||||
|
||||
case 'd':
|
||||
case 'D':
|
||||
id_and_lun = value;
|
||||
continue;
|
||||
|
||||
case 'z':
|
||||
locale = value.c_str();
|
||||
continue;
|
||||
|
||||
case 'F':
|
||||
if (const string error = piscsi_image.SetDefaultFolder(value); !error.empty()) {
|
||||
throw parser_exception(error);
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'R':
|
||||
int depth;
|
||||
if (!GetAsUnsignedInt(value, depth)) {
|
||||
throw parser_exception("Invalid image file scan depth " + value);
|
||||
}
|
||||
piscsi_image.SetDepth(depth);
|
||||
continue;
|
||||
|
||||
case 'n':
|
||||
name = value;
|
||||
continue;
|
||||
|
||||
case 'r':
|
||||
if (const string error = executor->SetReservedIds(value); !error.empty()) {
|
||||
throw parser_exception(error);
|
||||
}
|
||||
continue;
|
||||
|
||||
case 't':
|
||||
type = ParseDeviceType(value);
|
||||
continue;
|
||||
|
||||
case 1:
|
||||
// Encountered filename
|
||||
break;
|
||||
|
||||
default:
|
||||
throw parser_exception("Parser error");
|
||||
}
|
||||
|
||||
PbDeviceDefinition *device = command.add_devices();
|
||||
|
||||
if (!id_and_lun.empty()) {
|
||||
if (const string error = SetIdAndLun(*device, id_and_lun, ScsiController::LUN_MAX); !error.empty()) {
|
||||
throw parser_exception(error);
|
||||
}
|
||||
}
|
||||
|
||||
device->set_type(type);
|
||||
device->set_block_size(block_size);
|
||||
|
||||
ParseParameters(*device, value);
|
||||
|
||||
SetProductData(*device, name);
|
||||
|
||||
type = UNDEFINED;
|
||||
block_size = 0;
|
||||
name = "";
|
||||
id_and_lun = "";
|
||||
}
|
||||
|
||||
// Attach all specified devices
|
||||
command.set_operation(ATTACH);
|
||||
|
||||
if (CommandContext context(locale); !executor->ProcessCmd(context, command)) {
|
||||
throw parser_exception("Can't execute " + PbOperation_Name(command.operation()));
|
||||
}
|
||||
|
||||
// Display and log the device list
|
||||
PbServerInfo server_info;
|
||||
piscsi_response.GetDevices(controller_manager->GetAllDevices(), server_info, piscsi_image.GetDefaultFolder());
|
||||
const list<PbDevice>& devices = { server_info.devices_info().devices().begin(), server_info.devices_info().devices().end() };
|
||||
const string device_list = ListDevices(devices);
|
||||
LogDevices(device_list);
|
||||
cout << device_list << flush;
|
||||
}
|
||||
|
||||
bool Piscsi::ExecuteCommand(const CommandContext& context, const PbCommand& command)
|
||||
{
|
||||
if (!access_token.empty() && access_token != GetParam(command, "token")) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_AUTHENTICATION, UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (!PbOperation_IsValid(command.operation())) {
|
||||
LOGERROR("Received unknown command with operation opcode %d", command.operation())
|
||||
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION, UNKNOWN_OPERATION);
|
||||
}
|
||||
|
||||
LOGTRACE("Received %s command", PbOperation_Name(command.operation()).c_str())
|
||||
|
||||
PbResult result;
|
||||
ProtobufSerializer serializer;
|
||||
|
||||
switch(command.operation()) {
|
||||
case LOG_LEVEL: {
|
||||
const string log_level = GetParam(command, "level");
|
||||
if (const bool status = executor->SetLogLevel(log_level); !status) {
|
||||
context.ReturnLocalizedError(LocalizationKey::ERROR_LOG_LEVEL, log_level);
|
||||
}
|
||||
else {
|
||||
current_log_level = log_level;
|
||||
|
||||
context.ReturnStatus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DEFAULT_FOLDER: {
|
||||
if (const string status = piscsi_image.SetDefaultFolder(GetParam(command, "folder")); !status.empty()) {
|
||||
context.ReturnStatus(false, status);
|
||||
}
|
||||
else {
|
||||
context.ReturnStatus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DEVICES_INFO: {
|
||||
piscsi_response.GetDevicesInfo(controller_manager->GetAllDevices(), result, command,
|
||||
piscsi_image.GetDefaultFolder());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case DEVICE_TYPES_INFO: {
|
||||
result.set_allocated_device_types_info(piscsi_response.GetDeviceTypesInfo(result).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case SERVER_INFO: {
|
||||
result.set_allocated_server_info(piscsi_response.GetServerInfo(controller_manager->GetAllDevices(),
|
||||
result, executor->GetReservedIds(), current_log_level, piscsi_image.GetDefaultFolder(),
|
||||
GetParam(command, "folder_pattern"), GetParam(command, "file_pattern"),
|
||||
piscsi_image.GetDepth()).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case VERSION_INFO: {
|
||||
result.set_allocated_version_info(piscsi_response.GetVersionInfo(result).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case LOG_LEVEL_INFO: {
|
||||
result.set_allocated_log_level_info(piscsi_response.GetLogLevelInfo(result, current_log_level).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case DEFAULT_IMAGE_FILES_INFO: {
|
||||
result.set_allocated_image_files_info(piscsi_response.GetAvailableImages(result,
|
||||
piscsi_image.GetDefaultFolder(), GetParam(command, "folder_pattern"),
|
||||
GetParam(command, "file_pattern"), piscsi_image.GetDepth()).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case IMAGE_FILE_INFO: {
|
||||
if (string filename = GetParam(command, "file"); filename.empty()) {
|
||||
context.ReturnLocalizedError( LocalizationKey::ERROR_MISSING_FILENAME);
|
||||
}
|
||||
else {
|
||||
auto image_file = make_unique<PbImageFile>();
|
||||
const bool status = piscsi_response.GetImageFile(*image_file.get(), piscsi_image.GetDefaultFolder(), filename);
|
||||
if (status) {
|
||||
result.set_status(true);
|
||||
result.set_allocated_image_file_info(image_file.get());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
}
|
||||
else {
|
||||
context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_FILE_INFO);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case NETWORK_INTERFACES_INFO: {
|
||||
result.set_allocated_network_interfaces_info(piscsi_response.GetNetworkInterfacesInfo(result).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case MAPPING_INFO: {
|
||||
result.set_allocated_mapping_info(piscsi_response.GetMappingInfo(result).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPERATION_INFO: {
|
||||
result.set_allocated_operation_info(piscsi_response.GetOperationInfo(result,
|
||||
piscsi_image.GetDepth()).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case RESERVED_IDS_INFO: {
|
||||
result.set_allocated_reserved_ids_info(piscsi_response.GetReservedIds(result,
|
||||
executor->GetReservedIds()).release());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
break;
|
||||
}
|
||||
|
||||
case SHUT_DOWN: {
|
||||
if (executor->ShutDown(context, GetParam(command, "mode"))) {
|
||||
TerminationHandler(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Wait until we become idle
|
||||
const timespec ts = { .tv_sec = 0, .tv_nsec = 500'000'000};
|
||||
while (active) {
|
||||
nanosleep(&ts, nullptr);
|
||||
}
|
||||
|
||||
executor->ProcessCmd(context, command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int Piscsi::run(const vector<char *>& args)
|
||||
{
|
||||
GOOGLE_PROTOBUF_VERIFY_VERSION;
|
||||
|
||||
Banner(args);
|
||||
|
||||
int port = DEFAULT_PORT;
|
||||
optargs_type optargs;
|
||||
try {
|
||||
optargs = ParseArguments(args, port);
|
||||
}
|
||||
catch(const parser_exception& e) {
|
||||
cerr << "Error: " << e.what() << endl;
|
||||
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// current_log_level may have been updated by ParseArguments()
|
||||
executor->SetLogLevel(current_log_level);
|
||||
|
||||
// Create a thread-safe stdout logger to process the log messages
|
||||
const auto logger = stdout_color_mt("piscsi stdout logger");
|
||||
|
||||
if (!InitBus()) {
|
||||
cerr << "Error: Can't initialize bus" << endl;
|
||||
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// We need to wait to create the devices until after the bus/controller/etc objects have been created
|
||||
// TODO Try to resolve dependencies so that this work-around can be removed
|
||||
try {
|
||||
CreateInitialDevices(optargs);
|
||||
}
|
||||
catch(const parser_exception& e) {
|
||||
cerr << "Error: " << e.what() << endl;
|
||||
|
||||
Cleanup();
|
||||
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (!service.Init(&ExecuteCommand, port)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Signal handler to detach all devices on a KILL or TERM signal
|
||||
struct sigaction termination_handler;
|
||||
termination_handler.sa_handler = TerminationHandler;
|
||||
sigemptyset(&termination_handler.sa_mask);
|
||||
termination_handler.sa_flags = 0;
|
||||
sigaction(SIGINT, &termination_handler, nullptr);
|
||||
sigaction(SIGTERM, &termination_handler, nullptr);
|
||||
|
||||
// Set the affinity to a specific processor core
|
||||
FixCpu(3);
|
||||
|
||||
sched_param schparam;
|
||||
#ifdef USE_SEL_EVENT_ENABLE
|
||||
// Scheduling policy setting (highest priority)
|
||||
schparam.sched_priority = sched_get_priority_max(SCHED_FIFO);
|
||||
sched_setscheduler(0, SCHED_FIFO, &schparam);
|
||||
#else
|
||||
cout << "Note: No PiSCSI hardware support, only client interface calls are supported" << endl;
|
||||
#endif
|
||||
|
||||
// Start execution
|
||||
service.SetRunning(true);
|
||||
|
||||
// Main Loop
|
||||
while (service.IsRunning()) {
|
||||
#ifdef USE_SEL_EVENT_ENABLE
|
||||
// SEL signal polling
|
||||
if (!bus->PollSelectEvent()) {
|
||||
// Stop on interrupt
|
||||
if (errno == EINTR) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the bus
|
||||
bus->Acquire();
|
||||
#else
|
||||
bus->Acquire();
|
||||
if (!bus->GetSEL()) {
|
||||
const timespec ts = { .tv_sec = 0, .tv_nsec = 0};
|
||||
nanosleep(&ts, nullptr);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Wait until BSY is released as there is a possibility for the
|
||||
// initiator to assert it while setting the ID (for up to 3 seconds)
|
||||
WaitForNotBusy();
|
||||
|
||||
// Stop because the bus is busy or another device responded
|
||||
if (bus->GetBSY() || !bus->GetSEL()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int initiator_id = AbstractController::UNKNOWN_INITIATOR_ID;
|
||||
|
||||
// The initiator and target ID
|
||||
const uint8_t id_data = bus->GetDAT();
|
||||
|
||||
phase_t phase = phase_t::busfree;
|
||||
|
||||
// Identify the responsible controller
|
||||
auto controller = controller_manager->IdentifyController(id_data);
|
||||
if (controller != nullptr) {
|
||||
device_logger.SetIdAndLun(controller->GetTargetId(), -1);
|
||||
|
||||
initiator_id = controller->ExtractInitiatorId(id_data);
|
||||
|
||||
if (initiator_id != AbstractController::UNKNOWN_INITIATOR_ID) {
|
||||
device_logger.Trace("++++ Starting processing for initiator ID " + to_string(initiator_id));
|
||||
}
|
||||
else {
|
||||
device_logger.Trace("++++ Starting processing for unknown initiator ID");
|
||||
}
|
||||
|
||||
if (controller->Process(initiator_id) == phase_t::selection) {
|
||||
phase = phase_t::selection;
|
||||
}
|
||||
}
|
||||
|
||||
// Return to bus monitoring if the selection phase has not started
|
||||
if (phase != phase_t::selection) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start target device
|
||||
active = true;
|
||||
|
||||
#if !defined(USE_SEL_EVENT_ENABLE) && defined(__linux__)
|
||||
// Scheduling policy setting (highest priority)
|
||||
schparam.sched_priority = sched_get_priority_max(SCHED_FIFO);
|
||||
sched_setscheduler(0, SCHED_FIFO, &schparam);
|
||||
#endif
|
||||
|
||||
// Loop until the bus is free
|
||||
while (service.IsRunning()) {
|
||||
// Target drive
|
||||
phase = controller->Process(initiator_id);
|
||||
|
||||
// End when the bus is free
|
||||
if (phase == phase_t::busfree) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(USE_SEL_EVENT_ENABLE) && defined(__linux__)
|
||||
// Set the scheduling priority back to normal
|
||||
schparam.sched_priority = 0;
|
||||
sched_setscheduler(0, SCHED_OTHER, &schparam);
|
||||
#endif
|
||||
|
||||
// End the target travel
|
||||
active = false;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
void Piscsi::WaitForNotBusy() const
|
||||
{
|
||||
if (bus->GetBSY()) {
|
||||
const uint32_t now = SysTimer::GetTimerLow();
|
||||
|
||||
// Wait for 3s
|
||||
while ((SysTimer::GetTimerLow() - now) < 3'000'000) {
|
||||
bus->Acquire();
|
||||
|
||||
if (!bus->GetBSY()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "devices/device_logger.h"
|
||||
#include "piscsi/command_context.h"
|
||||
#include "piscsi/piscsi_service.h"
|
||||
#include "piscsi/piscsi_image.h"
|
||||
#include "piscsi/piscsi_response.h"
|
||||
#include "generated/piscsi_interface.pb.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class BUS;
|
||||
class ControllerManager;
|
||||
class PiscsiExecutor;
|
||||
|
||||
class Piscsi
|
||||
{
|
||||
using optargs_type = vector<pair<int, string>>;
|
||||
|
||||
static const int DEFAULT_PORT = 6868;
|
||||
|
||||
public:
|
||||
|
||||
Piscsi() = default;
|
||||
~Piscsi() = default;
|
||||
|
||||
int run(const vector<char *>&);
|
||||
|
||||
private:
|
||||
|
||||
void Banner(const vector<char *>&) const;
|
||||
bool InitBus() const;
|
||||
static void Cleanup();
|
||||
void ReadAccessToken(const string&) const;
|
||||
void LogDevices(string_view) const;
|
||||
PbDeviceType ParseDeviceType(const string&) const;
|
||||
static void TerminationHandler(int);
|
||||
optargs_type ParseArguments(const vector<char *>&, int&) const;
|
||||
void CreateInitialDevices(const optargs_type&) const;
|
||||
void WaitForNotBusy() const;
|
||||
|
||||
// TODO Should not be static and should be moved to PiscsiService
|
||||
static bool ExecuteCommand(const CommandContext&, const PbCommand&);
|
||||
|
||||
DeviceLogger device_logger;
|
||||
|
||||
// A static instance is needed because of the signal handler
|
||||
static inline shared_ptr<BUS> bus;
|
||||
|
||||
// TODO These fields should not be static
|
||||
|
||||
static inline PiscsiService service;
|
||||
|
||||
static inline PiscsiImage piscsi_image;
|
||||
|
||||
const static inline PiscsiResponse piscsi_response;
|
||||
|
||||
static inline shared_ptr<ControllerManager> controller_manager;
|
||||
|
||||
static inline shared_ptr<PiscsiExecutor> executor;
|
||||
|
||||
// Processing flag
|
||||
static inline volatile bool active;
|
||||
|
||||
// Some versions of spdlog do not support get_log_level(), so we have to remember the level
|
||||
static inline string current_log_level = "info";
|
||||
|
||||
static inline string access_token;
|
||||
};
|
||||
@@ -0,0 +1,779 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#include "shared/log.h"
|
||||
#include "shared/piscsi_util.h"
|
||||
#include "shared/protobuf_util.h"
|
||||
#include "shared/piscsi_exceptions.h"
|
||||
#include "controllers/controller_manager.h"
|
||||
#include "controllers/scsi_controller.h"
|
||||
#include "devices/device_logger.h"
|
||||
#include "devices/device_factory.h"
|
||||
#include "devices/primary_device.h"
|
||||
#include "devices/disk.h"
|
||||
#include "piscsi_service.h"
|
||||
#include "piscsi_image.h"
|
||||
#include "localizer.h"
|
||||
#include "command_context.h"
|
||||
#include "piscsi_executor.h"
|
||||
#include <sstream>
|
||||
|
||||
using namespace spdlog;
|
||||
using namespace protobuf_util;
|
||||
using namespace piscsi_util;
|
||||
|
||||
bool PiscsiExecutor::ProcessDeviceCmd(const CommandContext& context, const PbDeviceDefinition& pb_device,
|
||||
const PbCommand& command, bool dryRun)
|
||||
{
|
||||
PrintCommand(command, pb_device, dryRun);
|
||||
|
||||
const int id = pb_device.id();
|
||||
const int lun = pb_device.unit();
|
||||
|
||||
if (!ValidateIdAndLun(context, id, lun)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const PbOperation operation = command.operation();
|
||||
|
||||
// For all commands except ATTACH the device and LUN must exist
|
||||
if (operation != ATTACH && !VerifyExistingIdAndLun(context, id, lun)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto device = controller_manager.GetDeviceByIdAndLun(id, lun);
|
||||
|
||||
if (!ValidateOperationAgainstDevice(context, *device, operation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case START:
|
||||
return Start(*device, dryRun);
|
||||
|
||||
case STOP:
|
||||
return Stop(*device, dryRun);
|
||||
|
||||
case ATTACH:
|
||||
return Attach(context, pb_device, dryRun);
|
||||
|
||||
case DETACH:
|
||||
return Detach(context, device, dryRun);
|
||||
|
||||
case INSERT:
|
||||
return Insert(context, pb_device, device, dryRun);
|
||||
|
||||
case EJECT:
|
||||
return Eject(*device, dryRun);
|
||||
|
||||
case PROTECT:
|
||||
return Protect(*device, dryRun);
|
||||
|
||||
case UNPROTECT:
|
||||
return Unprotect(*device, dryRun);
|
||||
break;
|
||||
|
||||
case CHECK_AUTHENTICATION:
|
||||
case NO_OPERATION:
|
||||
// Do nothing, just log
|
||||
LOGTRACE("Received %s command", PbOperation_Name(operation).c_str())
|
||||
break;
|
||||
|
||||
default:
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::ProcessCmd(const CommandContext& context, const PbCommand& command)
|
||||
{
|
||||
switch (command.operation()) {
|
||||
case DETACH_ALL:
|
||||
DetachAll();
|
||||
return context.ReturnStatus();
|
||||
|
||||
case RESERVE_IDS: {
|
||||
const string ids = GetParam(command, "ids");
|
||||
if (const string error = SetReservedIds(ids); !error.empty()) {
|
||||
return context.ReturnStatus(false, error);
|
||||
}
|
||||
|
||||
return context.ReturnStatus();
|
||||
}
|
||||
|
||||
case CREATE_IMAGE:
|
||||
return piscsi_image.CreateImage(context, command);
|
||||
|
||||
case DELETE_IMAGE:
|
||||
return piscsi_image.DeleteImage(context, command);
|
||||
|
||||
case RENAME_IMAGE:
|
||||
return piscsi_image.RenameImage(context, command);
|
||||
|
||||
case COPY_IMAGE:
|
||||
return piscsi_image.CopyImage(context, command);
|
||||
|
||||
case PROTECT_IMAGE:
|
||||
case UNPROTECT_IMAGE:
|
||||
return piscsi_image.SetImagePermissions(context, command);
|
||||
|
||||
default:
|
||||
// This is a device-specific command handled below
|
||||
break;
|
||||
}
|
||||
|
||||
// Remember the list of reserved files, than run the dry run
|
||||
const auto& reserved_files = StorageDevice::GetReservedFiles();
|
||||
for (const auto& device : command.devices()) {
|
||||
if (!ProcessDeviceCmd(context, device, command, true)) {
|
||||
// Dry run failed, restore the file list
|
||||
StorageDevice::SetReservedFiles(reserved_files);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the list of reserved files before proceeding
|
||||
StorageDevice::SetReservedFiles(reserved_files);
|
||||
|
||||
if (const string result = ValidateLunSetup(command); !result.empty()) {
|
||||
return context.ReturnStatus(false, result);
|
||||
}
|
||||
|
||||
for (const auto& device : command.devices()) {
|
||||
if (!ProcessDeviceCmd(context, device, command, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ATTACH and DETACH return the device list
|
||||
if (context.IsValid() && (command.operation() == ATTACH || command.operation() == DETACH)) {
|
||||
// A new command with an empty device list is required here in order to return data for all devices
|
||||
PbCommand cmd;
|
||||
PbResult result;
|
||||
piscsi_response.GetDevicesInfo(controller_manager.GetAllDevices(), result, cmd,
|
||||
piscsi_image.GetDefaultFolder());
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
return true;
|
||||
}
|
||||
|
||||
return context.ReturnStatus();
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::SetLogLevel(const string& log_level) const
|
||||
{
|
||||
int id = -1;
|
||||
int lun = -1;
|
||||
string level = log_level;
|
||||
|
||||
if (size_t separator_pos = log_level.find(COMPONENT_SEPARATOR); separator_pos != string::npos) {
|
||||
level = log_level.substr(0, separator_pos);
|
||||
|
||||
const string l = log_level.substr(separator_pos + 1);
|
||||
separator_pos = l.find(COMPONENT_SEPARATOR);
|
||||
if (separator_pos != string::npos) {
|
||||
const string error = ProcessId(l, ScsiController::LUN_MAX, id, lun);
|
||||
if (!error.empty()) {
|
||||
LOGWARN("Invalid device ID/LUN specifier '%s'", l.c_str())
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!GetAsUnsignedInt(l, id)) {
|
||||
LOGWARN("Invalid device ID specifier '%s'", l.c_str())
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto& it = log_level_mapping.find(level); it != log_level_mapping.end()) {
|
||||
set_level(it->second);
|
||||
}
|
||||
else {
|
||||
LOGWARN("Invalid log level '%s'", log_level.c_str())
|
||||
return false;
|
||||
}
|
||||
|
||||
DeviceLogger::SetLogIdAndLun(id, lun);
|
||||
|
||||
if (id != -1) {
|
||||
if (lun == -1) {
|
||||
LOGINFO("Set log level for device ID %d to '%s'", id, level.c_str())
|
||||
}
|
||||
else {
|
||||
LOGINFO("Set log level for device ID %d, LUN %d to '%s'", id, lun, level.c_str())
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGINFO("Set log level to '%s'", level.c_str())
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::Start(PrimaryDevice& device, bool dryRun) const
|
||||
{
|
||||
if (!dryRun) {
|
||||
LOGINFO("Start requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(), device.GetLun())
|
||||
|
||||
if (!device.Start()) {
|
||||
LOGWARN("Starting %s ID %d, unit %d failed", device.GetTypeString(), device.GetId(), device.GetLun())
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::Stop(PrimaryDevice& device, bool dryRun) const
|
||||
{
|
||||
if (!dryRun) {
|
||||
LOGINFO("Stop requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(), device.GetLun())
|
||||
|
||||
device.Stop();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::Eject(PrimaryDevice& device, bool dryRun) const
|
||||
{
|
||||
if (!dryRun) {
|
||||
LOGINFO("Eject requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(), device.GetLun())
|
||||
|
||||
if (!device.Eject(true)) {
|
||||
LOGWARN("Ejecting %s ID %d, unit %d failed", device.GetTypeString(), device.GetId(), device.GetLun())
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::Protect(PrimaryDevice& device, bool dryRun) const
|
||||
{
|
||||
if (!dryRun) {
|
||||
LOGINFO("Write protection requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(),
|
||||
device.GetLun())
|
||||
|
||||
device.SetProtected(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::Unprotect(PrimaryDevice& device, bool dryRun) const
|
||||
{
|
||||
if (!dryRun) {
|
||||
LOGINFO("Write unprotection requested for %s ID %d, unit %d", device.GetTypeString(), device.GetId(),
|
||||
device.GetLun())
|
||||
|
||||
device.SetProtected(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::Attach(const CommandContext& context, const PbDeviceDefinition& pb_device, bool dryRun)
|
||||
{
|
||||
const int id = pb_device.id();
|
||||
const int lun = pb_device.unit();
|
||||
const PbDeviceType type = pb_device.type();
|
||||
|
||||
if (lun >= ScsiController::LUN_MAX) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_LUN, to_string(lun), to_string(ScsiController::LUN_MAX));
|
||||
}
|
||||
|
||||
if (controller_manager.GetDeviceByIdAndLun(id, lun) != nullptr) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_DUPLICATE_ID, to_string(id), to_string(lun));
|
||||
}
|
||||
|
||||
if (reserved_ids.find(id) != reserved_ids.end()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_RESERVED_ID, to_string(id));
|
||||
}
|
||||
|
||||
const string filename = GetParam(pb_device, "file");
|
||||
|
||||
auto device = CreateDevice(context, type, lun, filename);
|
||||
if (device == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no filename was provided the medium is considered not inserted
|
||||
auto storage_device = dynamic_pointer_cast<StorageDevice>(device);
|
||||
device->SetRemoved(storage_device != nullptr ? filename.empty() : false);
|
||||
|
||||
if (!SetProductData(context, pb_device, *device)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetSectorSize(context, device, pb_device.block_size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
string full_path;
|
||||
if (device->SupportsFile()) {
|
||||
// Only with removable media drives, CD and MO the medium (=file) may be inserted later
|
||||
if (!device->IsRemovable() && filename.empty()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME, PbDeviceType_Name(type));
|
||||
}
|
||||
|
||||
if (!ValidateImageFile(context, *storage_device, filename, full_path)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only non read-only devices support protect/unprotect
|
||||
// This operation must not be executed before Open() because Open() overrides some settings.
|
||||
if (device->IsProtectable() && !device->IsReadOnly()) {
|
||||
device->SetProtected(pb_device.protected_());
|
||||
}
|
||||
|
||||
// Stop the dry run here, before actually attaching
|
||||
if (dryRun) {
|
||||
return true;
|
||||
}
|
||||
|
||||
unordered_map<string, string> params = { pb_device.params().begin(), pb_device.params().end() };
|
||||
if (!device->SupportsFile()) {
|
||||
// Clients like scsictl might have sent both "file" and "interfaces"
|
||||
params.erase("file");
|
||||
}
|
||||
|
||||
if (!device->Init(params)) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_INITIALIZATION, PbDeviceType_Name(device->GetType()),
|
||||
to_string(id), to_string(lun));
|
||||
}
|
||||
|
||||
if (storage_device != nullptr) {
|
||||
storage_device->ReserveFile(full_path, id, lun);
|
||||
}
|
||||
|
||||
if (!controller_manager.AttachToScsiController(id, device)) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_SCSI_CONTROLLER);
|
||||
}
|
||||
|
||||
string msg = "Attached ";
|
||||
if (device->IsReadOnly()) {
|
||||
msg += "read-only ";
|
||||
}
|
||||
else if (device->IsProtectable() && device->IsProtected()) {
|
||||
msg += "protected ";
|
||||
}
|
||||
msg += string(device->GetTypeString()) + " device, ID " + to_string(id) + ", unit " + to_string(lun);
|
||||
LOGINFO("%s", msg.c_str())
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::Insert(const CommandContext& context, const PbDeviceDefinition& pb_device,
|
||||
const shared_ptr<PrimaryDevice>& device, bool dryRun) const
|
||||
{
|
||||
auto storage_device = dynamic_pointer_cast<StorageDevice>(device);
|
||||
if (storage_device == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!storage_device->IsRemoved()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_EJECT_REQUIRED);
|
||||
}
|
||||
|
||||
if (!pb_device.vendor().empty() || !pb_device.product().empty() || !pb_device.revision().empty()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_DEVICE_NAME_UPDATE);
|
||||
}
|
||||
|
||||
const string filename = GetParam(pb_device, "file");
|
||||
if (filename.empty()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME);
|
||||
}
|
||||
|
||||
// Stop the dry run here, before modifying the device
|
||||
if (dryRun) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGINFO("Insert %sfile '%s' requested into %s ID %d, unit %d", pb_device.protected_() ? "protected " : "",
|
||||
filename.c_str(), storage_device->GetTypeString(), pb_device.id(), pb_device.unit())
|
||||
|
||||
if (!SetSectorSize(context, storage_device, pb_device.block_size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
string full_path;
|
||||
if (!ValidateImageFile(context, *storage_device, filename, full_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
storage_device->SetProtected(pb_device.protected_());
|
||||
storage_device->ReserveFile(full_path, storage_device->GetId(), storage_device->GetLun());
|
||||
storage_device->SetMediumChanged(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::Detach(const CommandContext& context, const shared_ptr<PrimaryDevice>& device, bool dryRun) const
|
||||
{
|
||||
auto controller = controller_manager.FindController(device->GetId());
|
||||
if (controller == nullptr) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
|
||||
}
|
||||
|
||||
// LUN 0 can only be detached if there is no other LUN anymore
|
||||
if (!device->GetLun() && controller->GetLunCount() > 1) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_LUN0);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
// Remember the ID before it gets invalid when removing the device
|
||||
const int id = device->GetId();
|
||||
|
||||
if (!controller->RemoveDevice(device)) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
|
||||
}
|
||||
|
||||
// If no LUN is left also delete the controller
|
||||
if (!controller->GetLunCount() && !controller_manager.DeleteController(controller)) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
|
||||
}
|
||||
|
||||
if (auto storage_device = dynamic_pointer_cast<StorageDevice>(device); storage_device != nullptr) {
|
||||
storage_device->UnreserveFile();
|
||||
}
|
||||
|
||||
LOGINFO("%s", ("Detached " + string(device->GetTypeString()) + " device with ID " + to_string(id)
|
||||
+ ", unit " + to_string(device->GetLun())).c_str())
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PiscsiExecutor::DetachAll()
|
||||
{
|
||||
controller_manager.DeleteAllControllers();
|
||||
StorageDevice::UnreserveAll();
|
||||
|
||||
LOGINFO("Detached all devices")
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::ShutDown(const CommandContext& context, const string& mode) {
|
||||
if (mode.empty()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING);
|
||||
}
|
||||
|
||||
PbResult result;
|
||||
result.set_status(true);
|
||||
|
||||
// The PiSCSI shutdown mode is "rascsi" instead of "piscsi" for backwards compatibility
|
||||
if (mode == "rascsi") {
|
||||
LOGINFO("PiSCSI shutdown requested")
|
||||
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode != "system" && mode != "reboot") {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, mode);
|
||||
}
|
||||
|
||||
// The root user has UID 0
|
||||
if (getuid()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_PERMISSION);
|
||||
}
|
||||
|
||||
if (mode == "system") {
|
||||
LOGINFO("System shutdown requested")
|
||||
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
|
||||
DetachAll();
|
||||
|
||||
if (system("init 0") == -1) {
|
||||
LOGERROR("System shutdown failed: %s", strerror(errno))
|
||||
}
|
||||
}
|
||||
else if (mode == "reboot") {
|
||||
LOGINFO("System reboot requested")
|
||||
|
||||
serializer.SerializeMessage(context.GetFd(), result);
|
||||
|
||||
DetachAll();
|
||||
|
||||
if (system("init 6") == -1) {
|
||||
LOGERROR("System reboot failed: %s", strerror(errno))
|
||||
}
|
||||
}
|
||||
else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string PiscsiExecutor::SetReservedIds(string_view ids)
|
||||
{
|
||||
list<string> ids_to_reserve;
|
||||
stringstream ss(ids.data());
|
||||
string id;
|
||||
while (getline(ss, id, ',')) {
|
||||
if (!id.empty()) {
|
||||
ids_to_reserve.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
set<int> reserved;
|
||||
for (const string& id_to_reserve : ids_to_reserve) {
|
||||
int res_id;
|
||||
if (!GetAsUnsignedInt(id_to_reserve, res_id) || res_id > 7) {
|
||||
return "Invalid ID " + id_to_reserve;
|
||||
}
|
||||
|
||||
if (controller_manager.FindController(res_id) != nullptr) {
|
||||
return "ID " + id_to_reserve + " is currently in use";
|
||||
}
|
||||
|
||||
reserved.insert(res_id);
|
||||
}
|
||||
|
||||
reserved_ids = { reserved.begin(), reserved.end() };
|
||||
|
||||
if (!reserved_ids.empty()) {
|
||||
string s;
|
||||
bool isFirst = true;
|
||||
for (const auto& reserved_id : reserved) {
|
||||
if (!isFirst) {
|
||||
s += ", ";
|
||||
}
|
||||
isFirst = false;
|
||||
s += to_string(reserved_id);
|
||||
}
|
||||
|
||||
LOGINFO("Reserved ID(s) set to %s", s.c_str())
|
||||
}
|
||||
else {
|
||||
LOGINFO("Cleared reserved ID(s)")
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::ValidateImageFile(const CommandContext& context, StorageDevice& storage_device,
|
||||
const string& filename, string& full_path) const
|
||||
{
|
||||
if (filename.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (const auto [id1, lun1] = StorageDevice::GetIdsForReservedFile(filename); id1 != -1 || lun1 != -1) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_IN_USE, filename,
|
||||
to_string(id1), to_string(lun1));
|
||||
}
|
||||
|
||||
string effective_filename = filename;
|
||||
|
||||
if (!StorageDevice::FileExists(filename)) {
|
||||
// If the file does not exist search for it in the default image folder
|
||||
effective_filename = piscsi_image.GetDefaultFolder() + "/" + filename;
|
||||
|
||||
if (const auto [id2, lun2] = StorageDevice::GetIdsForReservedFile(effective_filename); id2 != -1 || lun2 != -1) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_IN_USE, filename,
|
||||
to_string(id2), to_string(lun2));
|
||||
}
|
||||
|
||||
if (!StorageDevice::FileExists(effective_filename)) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_FILE_OPEN, effective_filename);
|
||||
}
|
||||
}
|
||||
|
||||
storage_device.SetFilename(effective_filename);
|
||||
|
||||
if (storage_device.IsReadOnlyFile()) {
|
||||
// Permanently write-protected
|
||||
storage_device.SetReadOnly(true);
|
||||
storage_device.SetProtectable(false);
|
||||
}
|
||||
else {
|
||||
storage_device.SetReadOnly(false);
|
||||
storage_device.SetProtectable(true);
|
||||
}
|
||||
|
||||
storage_device.Open();
|
||||
|
||||
full_path = effective_filename;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PiscsiExecutor::PrintCommand(const PbCommand& command, const PbDeviceDefinition& pb_device, bool dryRun) const
|
||||
{
|
||||
const map<string, string, less<>> params = { command.params().begin(), command.params().end() };
|
||||
|
||||
ostringstream s;
|
||||
s << (dryRun ? "Validating" : "Executing");
|
||||
s << ": operation=" << PbOperation_Name(command.operation());
|
||||
|
||||
if (!params.empty()) {
|
||||
s << ", command params=";
|
||||
bool isFirst = true;
|
||||
for (const auto& [key, value]: params) {
|
||||
if (!isFirst) {
|
||||
s << ", ";
|
||||
}
|
||||
isFirst = false;
|
||||
string v = key != "token" ? value : "???";
|
||||
s << "'" << key << "=" << v << "'";
|
||||
}
|
||||
}
|
||||
|
||||
s << ", device id=" << pb_device.id() << ", lun=" << pb_device.unit() << ", type="
|
||||
<< PbDeviceType_Name(pb_device.type());
|
||||
|
||||
if (pb_device.params_size()) {
|
||||
s << ", device params=";
|
||||
bool isFirst = true;
|
||||
for (const auto& [key, value]: pb_device.params()) {
|
||||
if (!isFirst) {
|
||||
s << ":";
|
||||
}
|
||||
isFirst = false;
|
||||
s << "'" << key << "=" << value << "'";
|
||||
}
|
||||
}
|
||||
|
||||
s << ", vendor='" << pb_device.vendor() << "', product='" << pb_device.product()
|
||||
<< "', revision='" << pb_device.revision() << "', block size=" << pb_device.block_size();
|
||||
LOGINFO("%s", s.str().c_str())
|
||||
}
|
||||
|
||||
string PiscsiExecutor::ValidateLunSetup(const PbCommand& command) const
|
||||
{
|
||||
// Mapping of available LUNs (bit vector) to devices
|
||||
unordered_map<uint32_t, uint32_t> luns;
|
||||
|
||||
// Collect LUN bit vectors of new devices
|
||||
for (const auto& device : command.devices()) {
|
||||
luns[device.id()] |= 1 << device.unit();
|
||||
}
|
||||
|
||||
// Collect LUN bit vectors of existing devices
|
||||
for (const auto& device : controller_manager.GetAllDevices()) {
|
||||
luns[device->GetId()] |= 1 << device->GetLun();
|
||||
}
|
||||
|
||||
// LUN 0 must exist for all devices
|
||||
for (const auto& [id, lun]: luns) {
|
||||
if (!(lun & 0x01)) {
|
||||
return "LUN 0 is missing for device ID " + to_string(id);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::VerifyExistingIdAndLun(const CommandContext& context, int id, int lun) const
|
||||
{
|
||||
if (controller_manager.FindController(id) == nullptr) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_NON_EXISTING_DEVICE, to_string(id));
|
||||
}
|
||||
|
||||
if (controller_manager.GetDeviceByIdAndLun(id, lun) == nullptr) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_NON_EXISTING_UNIT, to_string(id), to_string(lun));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
shared_ptr<PrimaryDevice> PiscsiExecutor::CreateDevice(const CommandContext& context, const PbDeviceType type,
|
||||
int lun, const string& filename) const
|
||||
{
|
||||
auto device = device_factory.CreateDevice(type, lun, filename);
|
||||
if (device == nullptr) {
|
||||
if (type == UNDEFINED) {
|
||||
context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, filename);
|
||||
}
|
||||
else {
|
||||
context.ReturnLocalizedError(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, PbDeviceType_Name(type));
|
||||
}
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::SetSectorSize(const CommandContext& context, shared_ptr<PrimaryDevice> device, int block_size) const
|
||||
{
|
||||
if (block_size) {
|
||||
auto disk = dynamic_pointer_cast<Disk>(device);
|
||||
if (disk != nullptr && disk->IsSectorSizeConfigurable()) {
|
||||
if (!disk->SetConfiguredSectorSize(device_factory, block_size)) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_BLOCK_SIZE, to_string(block_size));
|
||||
}
|
||||
}
|
||||
else {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE,
|
||||
device->GetTypeString());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::ValidateOperationAgainstDevice(const CommandContext& context, const PrimaryDevice& device,
|
||||
const PbOperation& operation)
|
||||
{
|
||||
if ((operation == START || operation == STOP) && !device.IsStoppable()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, device.GetTypeString());
|
||||
}
|
||||
|
||||
if ((operation == INSERT || operation == EJECT) && !device.IsRemovable()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, device.GetTypeString());
|
||||
}
|
||||
|
||||
if ((operation == PROTECT || operation == UNPROTECT) && !device.IsProtectable()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, device.GetTypeString());
|
||||
}
|
||||
|
||||
if ((operation == PROTECT || operation == UNPROTECT) && !device.IsReady()) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_READY, device.GetTypeString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::ValidateIdAndLun(const CommandContext& context, int id, int lun)
|
||||
{
|
||||
// Validate the device ID and LUN
|
||||
if (id < 0) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_DEVICE_ID);
|
||||
}
|
||||
if (id >= ControllerManager::DEVICE_MAX) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_ID, to_string(id), to_string(ControllerManager::DEVICE_MAX - 1));
|
||||
}
|
||||
if (lun < 0 || lun >= ScsiController::LUN_MAX) {
|
||||
return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_LUN, to_string(lun), to_string(ScsiController::LUN_MAX - 1));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiExecutor::SetProductData(const CommandContext& context, const PbDeviceDefinition& pb_device,
|
||||
PrimaryDevice& device)
|
||||
{
|
||||
try {
|
||||
if (!pb_device.vendor().empty()) {
|
||||
device.SetVendor(pb_device.vendor());
|
||||
}
|
||||
if (!pb_device.product().empty()) {
|
||||
device.SetProduct(pb_device.product());
|
||||
}
|
||||
if (!pb_device.revision().empty()) {
|
||||
device.SetRevision(pb_device.revision());
|
||||
}
|
||||
}
|
||||
catch(const invalid_argument& e) {
|
||||
return context.ReturnStatus(false, e.what());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spdlog/spdlog.h"
|
||||
#include "shared/protobuf_serializer.h"
|
||||
#include "piscsi/piscsi_response.h"
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
|
||||
class PiscsiImage;
|
||||
class DeviceFactory;
|
||||
class ControllerManager;
|
||||
class PrimaryDevice;
|
||||
class CommandContext;
|
||||
|
||||
using namespace spdlog;
|
||||
|
||||
class PiscsiExecutor
|
||||
{
|
||||
public:
|
||||
|
||||
PiscsiExecutor(PiscsiImage& piscsi_image, ControllerManager& controller_manager)
|
||||
: piscsi_image(piscsi_image), controller_manager(controller_manager) {}
|
||||
~PiscsiExecutor() = default;
|
||||
|
||||
unordered_set<int> GetReservedIds() const { return reserved_ids; }
|
||||
|
||||
bool ProcessDeviceCmd(const CommandContext&, const PbDeviceDefinition&, const PbCommand&, bool);
|
||||
bool ProcessCmd(const CommandContext&, const PbCommand&);
|
||||
bool SetLogLevel(const string&) const;
|
||||
bool Start(PrimaryDevice&, bool) const;
|
||||
bool Stop(PrimaryDevice&, bool) const;
|
||||
bool Eject(PrimaryDevice&, bool) const;
|
||||
bool Protect(PrimaryDevice&, bool) const;
|
||||
bool Unprotect(PrimaryDevice&, bool) const;
|
||||
bool Attach(const CommandContext&, const PbDeviceDefinition&, bool);
|
||||
bool Insert(const CommandContext&, const PbDeviceDefinition&, const shared_ptr<PrimaryDevice>&, bool) const;
|
||||
bool Detach(const CommandContext&, const shared_ptr<PrimaryDevice>&, bool) const;
|
||||
void DetachAll();
|
||||
bool ShutDown(const CommandContext&, const string&);
|
||||
string SetReservedIds(string_view);
|
||||
bool ValidateImageFile(const CommandContext&, StorageDevice&, const string&, string&) const;
|
||||
void PrintCommand(const PbCommand&, const PbDeviceDefinition&, bool) const;
|
||||
string ValidateLunSetup(const PbCommand&) const;
|
||||
bool VerifyExistingIdAndLun(const CommandContext&, int, int) const;
|
||||
shared_ptr<PrimaryDevice> CreateDevice(const CommandContext&, const PbDeviceType, int, const string&) const;
|
||||
bool SetSectorSize(const CommandContext&, shared_ptr<PrimaryDevice>, int) const;
|
||||
|
||||
static bool ValidateOperationAgainstDevice(const CommandContext&, const PrimaryDevice&, const PbOperation&);
|
||||
static bool ValidateIdAndLun(const CommandContext&, int, int);
|
||||
static bool SetProductData(const CommandContext&, const PbDeviceDefinition&, PrimaryDevice&);
|
||||
|
||||
private:
|
||||
|
||||
const PiscsiResponse piscsi_response;
|
||||
|
||||
PiscsiImage& piscsi_image;
|
||||
|
||||
ControllerManager& controller_manager;
|
||||
|
||||
const DeviceFactory device_factory;
|
||||
|
||||
const ProtobufSerializer serializer;
|
||||
|
||||
unordered_set<int> reserved_ids;
|
||||
|
||||
static inline const unordered_map<string, level::level_enum> log_level_mapping = {
|
||||
{ "trace", level::trace },
|
||||
{ "debug", level::debug },
|
||||
{ "info", level::info },
|
||||
{ "warn", level::warn },
|
||||
{ "err", level::err },
|
||||
{ "off", level::off }
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,441 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#include "shared/log.h"
|
||||
#include "shared/protobuf_util.h"
|
||||
#include "devices/disk.h"
|
||||
#include "command_context.h"
|
||||
#include "piscsi_image.h"
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace std;
|
||||
using namespace filesystem;
|
||||
using namespace piscsi_interface;
|
||||
using namespace protobuf_util;
|
||||
|
||||
PiscsiImage::PiscsiImage()
|
||||
{
|
||||
// ~/images is the default folder for device image files, for the root user it is /home/pi/images
|
||||
default_folder = GetHomeDir() + "/images";
|
||||
}
|
||||
|
||||
bool PiscsiImage::CheckDepth(string_view filename) const
|
||||
{
|
||||
return count(filename.begin(), filename.end(), '/') <= depth;
|
||||
}
|
||||
|
||||
bool PiscsiImage::CreateImageFolder(const CommandContext& context, const string& filename) const
|
||||
{
|
||||
if (const size_t filename_start = filename.rfind('/'); filename_start != string::npos) {
|
||||
const auto folder = path(filename.substr(0, filename_start));
|
||||
|
||||
// Checking for existence first prevents an error if the top-level folder is a softlink
|
||||
if (error_code error; exists(folder, error)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
create_directories(folder);
|
||||
|
||||
return ChangeOwner(context, folder, false);
|
||||
}
|
||||
catch(const filesystem_error& e) {
|
||||
return context.ReturnStatus(false, "Can't create image folder '" + string(folder) + "': " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
string PiscsiImage::SetDefaultFolder(const string& f)
|
||||
{
|
||||
if (f.empty()) {
|
||||
return "Can't set default image folder: Missing folder name";
|
||||
}
|
||||
|
||||
string folder = f;
|
||||
|
||||
// If a relative path is specified, the path is assumed to be relative to the user's home directory
|
||||
if (folder[0] != '/') {
|
||||
folder = GetHomeDir() + "/" + folder;
|
||||
}
|
||||
else {
|
||||
if (folder.find("/home/") != 0) {
|
||||
return "Default image folder must be located in '/home/'";
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a potential symlink
|
||||
auto p = path(folder);
|
||||
if (error_code error; is_symlink(p, error)) {
|
||||
p = read_symlink(p);
|
||||
}
|
||||
|
||||
if (error_code error; !is_directory(p, error)) {
|
||||
return "'" + string(p) + "' is not a valid folder";
|
||||
}
|
||||
|
||||
default_folder = string(p);
|
||||
|
||||
LOGINFO("Default image folder set to '%s'", default_folder.c_str())
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool PiscsiImage::CreateImage(const CommandContext& context, const PbCommand& command) const
|
||||
{
|
||||
const string filename = GetParam(command, "file");
|
||||
if (filename.empty()) {
|
||||
return context.ReturnStatus(false, "Can't create image file: Missing image filename");
|
||||
}
|
||||
|
||||
if (!CheckDepth(filename)) {
|
||||
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
|
||||
}
|
||||
|
||||
const string full_filename = GetFullName(filename);
|
||||
if (!IsValidDstFilename(full_filename)) {
|
||||
return context.ReturnStatus(false, "Can't create image file: '" + full_filename + "': File already exists");
|
||||
}
|
||||
|
||||
const string size = GetParam(command, "size");
|
||||
if (size.empty()) {
|
||||
return context.ReturnStatus(false, "Can't create image file '" + full_filename + "': Missing file size");
|
||||
}
|
||||
|
||||
off_t len;
|
||||
try {
|
||||
len = stoull(size);
|
||||
}
|
||||
catch(const invalid_argument&) {
|
||||
return context.ReturnStatus(false, "Can't create image file '" + full_filename + "': Invalid file size " + size);
|
||||
}
|
||||
catch(const out_of_range&) {
|
||||
return context.ReturnStatus(false, "Can't create image file '" + full_filename + "': Invalid file size " + size);
|
||||
}
|
||||
if (len < 512 || (len & 0x1ff)) {
|
||||
return context.ReturnStatus(false, "Invalid image file size " + to_string(len) + " (not a multiple of 512)");
|
||||
}
|
||||
|
||||
if (!CreateImageFolder(context, full_filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool read_only = GetParam(command, "read_only") == "true";
|
||||
|
||||
error_code error;
|
||||
path file(full_filename);
|
||||
try {
|
||||
ofstream s(file);
|
||||
s.close();
|
||||
|
||||
if (!ChangeOwner(context, file, read_only)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
resize_file(file, len);
|
||||
}
|
||||
catch(const filesystem_error& e) {
|
||||
remove(file, error);
|
||||
|
||||
return context.ReturnStatus(false, "Can't create image file '" + full_filename + "': " + e.what());
|
||||
}
|
||||
|
||||
LOGINFO("%s", string("Created " + string(read_only ? "read-only " : "") + "image file '" + full_filename +
|
||||
"' with a size of " + to_string(len) + " bytes").c_str())
|
||||
|
||||
return context.ReturnStatus();
|
||||
}
|
||||
|
||||
bool PiscsiImage::DeleteImage(const CommandContext& context, const PbCommand& command) const
|
||||
{
|
||||
const string filename = GetParam(command, "file");
|
||||
if (filename.empty()) {
|
||||
return context.ReturnStatus(false, "Missing image filename");
|
||||
}
|
||||
|
||||
if (!CheckDepth(filename)) {
|
||||
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
|
||||
}
|
||||
|
||||
const auto full_filename = path(GetFullName(filename));
|
||||
|
||||
if (!exists(full_filename)) {
|
||||
return context.ReturnStatus(false, "Image file '" + string(full_filename) + "' does not exist");
|
||||
}
|
||||
|
||||
const auto [id, lun] = StorageDevice::GetIdsForReservedFile(full_filename);
|
||||
if (id != -1 || lun != -1) {
|
||||
return context.ReturnStatus(false, "Can't delete image file '" + string(full_filename) +
|
||||
"', it is currently being used by device ID " + to_string(id) + ", LUN " + to_string(lun));
|
||||
}
|
||||
|
||||
if (error_code error; !remove(full_filename, error)) {
|
||||
return context.ReturnStatus(false, "Can't delete image file '" + string(full_filename) + "'");
|
||||
}
|
||||
|
||||
// Delete empty subfolders
|
||||
size_t last_slash = filename.rfind('/');
|
||||
while (last_slash != string::npos) {
|
||||
const string folder = filename.substr(0, last_slash);
|
||||
const auto full_folder = path(GetFullName(folder));
|
||||
|
||||
if (error_code error; !filesystem::is_empty(full_folder, error) || error) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (error_code error; !remove(full_folder)) {
|
||||
return context.ReturnStatus(false, "Can't delete empty image folder '" + string(full_folder) + "'");
|
||||
}
|
||||
|
||||
last_slash = folder.rfind('/');
|
||||
}
|
||||
|
||||
LOGINFO("Deleted image file '%s'", full_filename.c_str())
|
||||
|
||||
return context.ReturnStatus();
|
||||
}
|
||||
|
||||
bool PiscsiImage::RenameImage(const CommandContext& context, const PbCommand& command) const
|
||||
{
|
||||
string from;
|
||||
string to;
|
||||
if (!ValidateParams(context, command, "rename/move", from, to)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto [id, lun] = StorageDevice::GetIdsForReservedFile(from);
|
||||
if (id != -1 || lun != -1) {
|
||||
return context.ReturnStatus(false, "Can't rename/move image file '" + from +
|
||||
"', it is currently being used by device ID " + to_string(id) + ", LUN " + to_string(lun));
|
||||
}
|
||||
|
||||
if (!CreateImageFolder(context, to)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
rename(path(from), path(to));
|
||||
}
|
||||
catch(const filesystem_error& e) {
|
||||
return context.ReturnStatus(false, "Can't rename/move image file '" + from + "' to '" + to + "': " + e.what());
|
||||
}
|
||||
|
||||
LOGINFO("Renamed/Moved image file '%s' to '%s'", from.c_str(), to.c_str())
|
||||
|
||||
return context.ReturnStatus();
|
||||
}
|
||||
|
||||
bool PiscsiImage::CopyImage(const CommandContext& context, const PbCommand& command) const
|
||||
{
|
||||
string from;
|
||||
string to;
|
||||
if (!ValidateParams(context, command, "copy", from, to)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (access(from.c_str(), R_OK)) {
|
||||
return context.ReturnStatus(false, "Can't read source image file '" + from + "'");
|
||||
}
|
||||
|
||||
const auto [id, lun] = StorageDevice::GetIdsForReservedFile(from);
|
||||
if (id != -1 || lun != -1) {
|
||||
return context.ReturnStatus(false, "Can't copy image file '" + from +
|
||||
"', it is currently being used by device ID " + to_string(id) + ", LUN " + to_string(lun));
|
||||
}
|
||||
|
||||
if (!CreateImageFolder(context, to)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
path f(from);
|
||||
path t(to);
|
||||
|
||||
// Symbolic links need a special handling
|
||||
if (error_code error; is_symlink(f, error)) {
|
||||
try {
|
||||
copy_symlink(f, t);
|
||||
}
|
||||
catch(const filesystem_error& e) {
|
||||
return context.ReturnStatus(false, "Can't copy image file symlink '" + from + "': " + e.what());
|
||||
}
|
||||
|
||||
LOGINFO("Copied image file symlink '%s' to '%s'", from.c_str(), to.c_str())
|
||||
|
||||
return context.ReturnStatus();
|
||||
}
|
||||
|
||||
try {
|
||||
copy_file(f, t);
|
||||
|
||||
permissions(t, GetParam(command, "read_only") == "true" ?
|
||||
perms::owner_read | perms::group_read | perms::others_read :
|
||||
perms::owner_read | perms::group_read | perms::others_read |
|
||||
perms::owner_write | perms::group_write);
|
||||
}
|
||||
catch(const filesystem_error& e) {
|
||||
return context.ReturnStatus(false, "Can't copy image file '" + from + "' to '" + to + "': " + e.what());
|
||||
}
|
||||
|
||||
LOGINFO("Copied image file '%s' to '%s'", from.c_str(), to.c_str())
|
||||
|
||||
return context.ReturnStatus();
|
||||
}
|
||||
|
||||
bool PiscsiImage::SetImagePermissions(const CommandContext& context, const PbCommand& command) const
|
||||
{
|
||||
string filename = GetParam(command, "file");
|
||||
if (filename.empty()) {
|
||||
return context.ReturnStatus(false, "Missing image filename");
|
||||
}
|
||||
|
||||
if (!CheckDepth(filename)) {
|
||||
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
|
||||
}
|
||||
|
||||
filename = GetFullName(filename);
|
||||
if (!IsValidSrcFilename(filename)) {
|
||||
return context.ReturnStatus(false, "Can't modify image file '" + filename + "': Invalid name or type");
|
||||
}
|
||||
|
||||
const bool protect = command.operation() == PROTECT_IMAGE;
|
||||
|
||||
try {
|
||||
permissions(path(filename), protect ?
|
||||
perms::owner_read | perms::group_read | perms::others_read :
|
||||
perms::owner_read | perms::group_read | perms::others_read |
|
||||
perms::owner_write | perms::group_write);
|
||||
}
|
||||
catch(const filesystem_error& e) {
|
||||
return context.ReturnStatus(false, "Can't " + string(protect ? "protect" : "unprotect") + " image file '" +
|
||||
filename + "': " + e.what());
|
||||
}
|
||||
|
||||
if (protect) {
|
||||
LOGINFO("Protected image file '%s'", filename.c_str())
|
||||
}
|
||||
else {
|
||||
LOGINFO("Unprotected image file '%s'", filename.c_str())
|
||||
}
|
||||
|
||||
return context.ReturnStatus();
|
||||
}
|
||||
|
||||
bool PiscsiImage::ValidateParams(const CommandContext& context, const PbCommand& command, const string& operation,
|
||||
string& from, string& to) const
|
||||
{
|
||||
from = GetParam(command, "from");
|
||||
if (from.empty()) {
|
||||
return context.ReturnStatus(false, "Can't " + operation + " image file: Missing source filename");
|
||||
}
|
||||
|
||||
if (!CheckDepth(from)) {
|
||||
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + from + "'").c_str());
|
||||
}
|
||||
|
||||
from = GetFullName(from);
|
||||
if (!IsValidSrcFilename(from)) {
|
||||
return context.ReturnStatus(false, "Can't " + operation + " image file: '" + from + "': Invalid name or type");
|
||||
}
|
||||
|
||||
to = GetParam(command, "to");
|
||||
if (to.empty()) {
|
||||
return context.ReturnStatus(false, "Can't " + operation + " image file '" + from + "': Missing destination filename");
|
||||
}
|
||||
|
||||
if (!CheckDepth(to)) {
|
||||
return context.ReturnStatus(false, ("Invalid folder hierarchy depth '" + to + "'").c_str());
|
||||
}
|
||||
|
||||
to = GetFullName(to);
|
||||
if (!IsValidDstFilename(to)) {
|
||||
return context.ReturnStatus(false, "Can't " + operation + " image file '" + from + "' to '" + to + "': File already exists");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PiscsiImage::IsValidSrcFilename(const string& filename)
|
||||
{
|
||||
// Source file must exist and must be a regular file or a symlink
|
||||
path file(filename);
|
||||
return is_regular_file(file) || is_symlink(file);
|
||||
}
|
||||
|
||||
bool PiscsiImage::IsValidDstFilename(const string& filename)
|
||||
{
|
||||
// Destination file must not yet exist
|
||||
try {
|
||||
return !exists(path(filename));
|
||||
}
|
||||
catch(const filesystem_error&) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool PiscsiImage::ChangeOwner(const CommandContext& context, const path& filename, bool read_only)
|
||||
{
|
||||
const auto [uid, gid] = GetUidAndGid();
|
||||
if (chown(filename.c_str(), uid, gid)) {
|
||||
// Remember the current error before the next filesystem operation
|
||||
const int e = errno;
|
||||
|
||||
error_code error;
|
||||
remove(filename, error);
|
||||
|
||||
return context.ReturnStatus(false, "Can't change ownership of '" + string(filename) + "': " + strerror(e));
|
||||
}
|
||||
|
||||
permissions(filename, read_only ?
|
||||
perms::owner_read | perms::group_read | perms::others_read :
|
||||
perms::owner_read | perms::group_read | perms::others_read |
|
||||
perms::owner_write | perms::group_write);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
string PiscsiImage::GetHomeDir()
|
||||
{
|
||||
const auto [uid, gid] = GetUidAndGid();
|
||||
|
||||
passwd pwd = {};
|
||||
passwd *p_pwd;
|
||||
array<char, 256> pwbuf;
|
||||
|
||||
if (uid && !getpwuid_r(uid, &pwd, pwbuf.data(), pwbuf.size(), &p_pwd)) {
|
||||
return pwd.pw_dir;
|
||||
}
|
||||
else {
|
||||
return "/home/pi";
|
||||
}
|
||||
}
|
||||
|
||||
pair<int, int> PiscsiImage::GetUidAndGid()
|
||||
{
|
||||
int uid = getuid();
|
||||
if (const char *sudo_user = getenv("SUDO_UID"); sudo_user != nullptr) {
|
||||
uid = stoi(sudo_user);
|
||||
}
|
||||
|
||||
passwd pwd = {};
|
||||
passwd *p_pwd;
|
||||
array<char, 256> pwbuf;
|
||||
|
||||
int gid = -1;
|
||||
if (!getpwuid_r(uid, &pwd, pwbuf.data(), pwbuf.size(), &p_pwd)) {
|
||||
gid = pwd.pw_gid;
|
||||
}
|
||||
|
||||
return make_pair(uid, gid);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "generated/piscsi_interface.pb.h"
|
||||
#include "command_context.h"
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace std;
|
||||
using namespace filesystem;
|
||||
using namespace piscsi_interface;
|
||||
|
||||
class PiscsiImage
|
||||
{
|
||||
public:
|
||||
|
||||
PiscsiImage();
|
||||
~PiscsiImage() = default;
|
||||
|
||||
void SetDepth(int d) { depth = d; }
|
||||
int GetDepth() const { return depth; }
|
||||
string GetDefaultFolder() const { return default_folder; }
|
||||
string SetDefaultFolder(const string&);
|
||||
bool CreateImage(const CommandContext&, const PbCommand&) const;
|
||||
bool DeleteImage(const CommandContext&, const PbCommand&) const;
|
||||
bool RenameImage(const CommandContext&, const PbCommand&) const;
|
||||
bool CopyImage(const CommandContext&, const PbCommand&) const;
|
||||
bool SetImagePermissions(const CommandContext&, const PbCommand&) const;
|
||||
|
||||
private:
|
||||
|
||||
bool CheckDepth(string_view) const;
|
||||
string GetFullName(const string& filename) const { return default_folder + "/" + filename; }
|
||||
bool CreateImageFolder(const CommandContext&, const string&) const;
|
||||
bool ValidateParams(const CommandContext&, const PbCommand&, const string&, string&, string&) const;
|
||||
|
||||
static bool IsValidSrcFilename(const string&);
|
||||
static bool IsValidDstFilename(const string&);
|
||||
static bool ChangeOwner(const CommandContext&, const path&, bool);
|
||||
static string GetHomeDir();
|
||||
static pair<int, int> GetUidAndGid();
|
||||
|
||||
string default_folder;
|
||||
|
||||
int depth = 1;
|
||||
};
|
||||
@@ -0,0 +1,551 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#include "shared/log.h"
|
||||
#include "shared/protobuf_util.h"
|
||||
#include "shared/piscsi_version.h"
|
||||
#include "devices/disk.h"
|
||||
#include "devices/device_factory.h"
|
||||
#include "generated/piscsi_interface.pb.h"
|
||||
#include "piscsi_response.h"
|
||||
#include <filesystem>
|
||||
|
||||
using namespace std;
|
||||
using namespace filesystem;
|
||||
using namespace piscsi_interface;
|
||||
using namespace protobuf_util;
|
||||
|
||||
unique_ptr<PbDeviceProperties> PiscsiResponse::GetDeviceProperties(const Device& device) const
|
||||
{
|
||||
auto properties = make_unique<PbDeviceProperties>();
|
||||
|
||||
// Currently there is only a SCSI controller, i.e. there can always be 32 LUNs
|
||||
properties->set_luns(32);
|
||||
properties->set_read_only(device.IsReadOnly());
|
||||
properties->set_protectable(device.IsProtectable());
|
||||
properties->set_stoppable(device.IsStoppable());
|
||||
properties->set_removable(device.IsRemovable());
|
||||
properties->set_lockable(device.IsLockable());
|
||||
properties->set_supports_file(device.SupportsFile());
|
||||
properties->set_supports_params(device.SupportsParams());
|
||||
|
||||
if (device.SupportsParams()) {
|
||||
for (const auto& [key, value] : device_factory.GetDefaultParams(device.GetType())) {
|
||||
auto& map = *properties->mutable_default_params();
|
||||
map[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& block_size : device_factory.GetSectorSizes(device.GetType())) {
|
||||
properties->add_block_sizes(block_size);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
void PiscsiResponse::GetDeviceTypeProperties(PbDeviceTypesInfo& device_types_info, PbDeviceType type) const
|
||||
{
|
||||
auto type_properties = device_types_info.add_properties();
|
||||
type_properties->set_type(type);
|
||||
const auto device = device_factory.CreateDevice(type, 0, "");
|
||||
type_properties->set_allocated_properties(GetDeviceProperties(*device).release());
|
||||
} //NOSONAR The allocated memory is managed by protobuf
|
||||
|
||||
void PiscsiResponse::GetAllDeviceTypeProperties(PbDeviceTypesInfo& device_types_info) const
|
||||
{
|
||||
// Start with 2 instead of 1. 1 was the removed SASI drive type.
|
||||
int ordinal = 2;
|
||||
while (PbDeviceType_IsValid(ordinal)) {
|
||||
PbDeviceType type = UNDEFINED;
|
||||
PbDeviceType_Parse(PbDeviceType_Name((PbDeviceType)ordinal), &type);
|
||||
GetDeviceTypeProperties(device_types_info, type);
|
||||
ordinal++;
|
||||
}
|
||||
}
|
||||
|
||||
void PiscsiResponse::GetDevice(const Device& device, PbDevice& pb_device, const string& default_folder) const
|
||||
{
|
||||
pb_device.set_id(device.GetId());
|
||||
pb_device.set_unit(device.GetLun());
|
||||
pb_device.set_vendor(device.GetVendor());
|
||||
pb_device.set_product(device.GetProduct());
|
||||
pb_device.set_revision(device.GetRevision());
|
||||
pb_device.set_type(device.GetType());
|
||||
|
||||
pb_device.set_allocated_properties(GetDeviceProperties(device).release());
|
||||
|
||||
auto status = make_unique<PbDeviceStatus>().release(); //NOSONAR The allocated memory is managed by protobuf
|
||||
pb_device.set_allocated_status(status);
|
||||
status->set_protected_(device.IsProtected());
|
||||
status->set_stopped(device.IsStopped());
|
||||
status->set_removed(device.IsRemoved());
|
||||
status->set_locked(device.IsLocked());
|
||||
|
||||
if (device.SupportsParams()) { //NOSONAR The allocated memory is managed by protobuf
|
||||
for (const auto& [key, value] : device.GetParams()) {
|
||||
SetParam(pb_device, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto disk = dynamic_cast<const Disk*>(&device); disk) {
|
||||
pb_device.set_block_size(device.IsRemoved()? 0 : disk->GetSectorSizeInBytes());
|
||||
pb_device.set_block_count(device.IsRemoved() ? 0: disk->GetBlockCount());
|
||||
}
|
||||
|
||||
const auto storage_device = dynamic_cast<const StorageDevice *>(&device);
|
||||
if (storage_device != nullptr) {
|
||||
auto image_file = make_unique<PbImageFile>().release();
|
||||
GetImageFile(*image_file, default_folder, device.IsReady() ? storage_device->GetFilename() : "");
|
||||
pb_device.set_allocated_file(image_file);
|
||||
}
|
||||
} //NOSONAR The allocated memory is managed by protobuf
|
||||
|
||||
bool PiscsiResponse::GetImageFile(PbImageFile& image_file, const string& default_folder, const string& filename) const
|
||||
{
|
||||
if (!filename.empty()) {
|
||||
image_file.set_name(filename);
|
||||
image_file.set_type(device_factory.GetTypeForFile(filename));
|
||||
|
||||
const string f = filename[0] == '/' ? filename : default_folder + "/" + filename;
|
||||
|
||||
image_file.set_read_only(access(f.c_str(), W_OK));
|
||||
|
||||
// filesystem::file_size cannot be used here because gcc < 10.3.0 cannot handle files of more than 2 GiB
|
||||
if (struct stat st; !stat(f.c_str(), &st) && !S_ISDIR(st.st_mode)) {
|
||||
image_file.set_size(st.st_size);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void PiscsiResponse::GetAvailableImages(PbImageFilesInfo& image_files_info, const string& default_folder,
|
||||
const string& folder, const string& folder_pattern, const string& file_pattern, int scan_depth) const
|
||||
{
|
||||
if (scan_depth-- < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
string folder_pattern_lower = folder_pattern;
|
||||
transform(folder_pattern_lower.begin(), folder_pattern_lower.end(), folder_pattern_lower.begin(), ::tolower);
|
||||
|
||||
string file_pattern_lower = file_pattern;
|
||||
transform(file_pattern_lower.begin(), file_pattern_lower.end(), file_pattern_lower.begin(), ::tolower);
|
||||
|
||||
DIR *d = opendir(folder.c_str());
|
||||
if (d == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// C++ filesystem cannot be used here because gcc < 10.3.0 cannot handle files of more than 2 GiB
|
||||
const dirent *dir;
|
||||
while ((dir = readdir(d))) {
|
||||
string filename = GetNextImageFile(dir, folder);
|
||||
if (filename.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
string name_lower = dir->d_name;
|
||||
if (!file_pattern.empty()) {
|
||||
transform(name_lower.begin(), name_lower.end(), name_lower.begin(), ::tolower);
|
||||
}
|
||||
|
||||
if (dir->d_type == DT_DIR) {
|
||||
if (folder_pattern_lower.empty() || name_lower.find(folder_pattern_lower) != string::npos) {
|
||||
GetAvailableImages(image_files_info, default_folder, filename, folder_pattern,
|
||||
file_pattern, scan_depth);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file_pattern_lower.empty() || name_lower.find(file_pattern_lower) != string::npos) {
|
||||
if (auto image_file = make_unique<PbImageFile>(); GetImageFile(*image_file.get(), default_folder, filename)) {
|
||||
GetImageFile(*image_files_info.add_image_files(), default_folder,
|
||||
filename.substr(default_folder.length() + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
unique_ptr<PbImageFilesInfo> PiscsiResponse::GetAvailableImages(PbResult& result, const string& default_folder,
|
||||
const string& folder_pattern, const string& file_pattern, int scan_depth) const
|
||||
{
|
||||
auto image_files_info = make_unique<PbImageFilesInfo>();
|
||||
|
||||
image_files_info->set_default_image_folder(default_folder);
|
||||
image_files_info->set_depth(scan_depth);
|
||||
|
||||
GetAvailableImages(*image_files_info, default_folder, default_folder, folder_pattern,
|
||||
file_pattern, scan_depth);
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return image_files_info;
|
||||
}
|
||||
|
||||
void PiscsiResponse::GetAvailableImages(PbResult& result, PbServerInfo& server_info, const string& default_folder,
|
||||
const string& folder_pattern, const string& file_pattern, int scan_depth) const
|
||||
{
|
||||
auto image_files_info = GetAvailableImages(result, default_folder, folder_pattern, file_pattern, scan_depth);
|
||||
image_files_info->set_default_image_folder(default_folder);
|
||||
server_info.set_allocated_image_files_info(image_files_info.release());
|
||||
|
||||
result.set_status(true); //NOSONAR The allocated memory is managed by protobuf
|
||||
}
|
||||
|
||||
unique_ptr<PbReservedIdsInfo> PiscsiResponse::GetReservedIds(PbResult& result, const unordered_set<int>& ids) const
|
||||
{
|
||||
auto reserved_ids_info = make_unique<PbReservedIdsInfo>();
|
||||
for (const int id : ids) {
|
||||
reserved_ids_info->add_ids(id);
|
||||
}
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return reserved_ids_info;
|
||||
}
|
||||
|
||||
void PiscsiResponse::GetDevices(const unordered_set<shared_ptr<PrimaryDevice>>& devices, PbServerInfo& server_info,
|
||||
const string& default_folder) const
|
||||
{
|
||||
for (const auto& device : devices) {
|
||||
PbDevice *pb_device = server_info.mutable_devices_info()->add_devices();
|
||||
GetDevice(*device, *pb_device, default_folder);
|
||||
}
|
||||
}
|
||||
|
||||
void PiscsiResponse::GetDevicesInfo(const unordered_set<shared_ptr<PrimaryDevice>>& devices, PbResult& result,
|
||||
const PbCommand& command, const string& default_folder) const
|
||||
{
|
||||
set<id_set> id_sets;
|
||||
|
||||
// If no device list was provided in the command get information on all devices
|
||||
if (!command.devices_size()) {
|
||||
for (const auto& device : devices) {
|
||||
id_sets.insert(make_pair(device->GetId(), device->GetLun()));
|
||||
}
|
||||
}
|
||||
// Otherwise get information on the devices provided in the command
|
||||
else {
|
||||
id_sets = MatchDevices(devices, result, command);
|
||||
if (id_sets.empty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto devices_info = make_unique<PbDevicesInfo>();
|
||||
|
||||
for (const auto& [id, lun] : id_sets) {
|
||||
for (const auto& d : devices) {
|
||||
if (d->GetId() == id && d->GetLun() == lun) {
|
||||
GetDevice(*d, *devices_info->add_devices(), default_folder);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.set_allocated_devices_info(devices_info.release());
|
||||
result.set_status(true);
|
||||
}
|
||||
|
||||
unique_ptr<PbDeviceTypesInfo> PiscsiResponse::GetDeviceTypesInfo(PbResult& result) const
|
||||
{
|
||||
auto device_types_info = make_unique<PbDeviceTypesInfo>();
|
||||
|
||||
GetAllDeviceTypeProperties(*device_types_info);
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return device_types_info;
|
||||
}
|
||||
|
||||
unique_ptr<PbServerInfo> PiscsiResponse::GetServerInfo(const unordered_set<shared_ptr<PrimaryDevice>>& devices,
|
||||
PbResult& result, const unordered_set<int>& reserved_ids, const string& current_log_level,
|
||||
const string& default_folder, const string& folder_pattern, const string& file_pattern, int scan_depth) const
|
||||
{
|
||||
auto server_info = make_unique<PbServerInfo>();
|
||||
|
||||
server_info->set_allocated_version_info(GetVersionInfo(result).release());
|
||||
server_info->set_allocated_log_level_info(GetLogLevelInfo(result, current_log_level).release()); //NOSONAR The allocated memory is managed by protobuf
|
||||
GetAllDeviceTypeProperties(*server_info->mutable_device_types_info()); //NOSONAR The allocated memory is managed by protobuf
|
||||
GetAvailableImages(result, *server_info, default_folder, folder_pattern, file_pattern, scan_depth);
|
||||
server_info->set_allocated_network_interfaces_info(GetNetworkInterfacesInfo(result).release());
|
||||
server_info->set_allocated_mapping_info(GetMappingInfo(result).release()); //NOSONAR The allocated memory is managed by protobuf
|
||||
GetDevices(devices, *server_info, default_folder); //NOSONAR The allocated memory is managed by protobuf
|
||||
server_info->set_allocated_reserved_ids_info(GetReservedIds(result, reserved_ids).release());
|
||||
server_info->set_allocated_operation_info(GetOperationInfo(result, scan_depth).release()); //NOSONAR The allocated memory is managed by protobuf
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return server_info;
|
||||
}
|
||||
|
||||
unique_ptr<PbVersionInfo> PiscsiResponse::GetVersionInfo(PbResult& result) const
|
||||
{
|
||||
auto version_info = make_unique<PbVersionInfo>();
|
||||
|
||||
version_info->set_major_version(piscsi_major_version);
|
||||
version_info->set_minor_version(piscsi_minor_version);
|
||||
version_info->set_patch_version(piscsi_patch_version);
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return version_info;
|
||||
}
|
||||
|
||||
unique_ptr<PbLogLevelInfo> PiscsiResponse::GetLogLevelInfo(PbResult& result, const string& current_log_level) const
|
||||
{
|
||||
auto log_level_info = make_unique<PbLogLevelInfo>();
|
||||
|
||||
for (const auto& log_level : log_levels) {
|
||||
log_level_info->add_log_levels(log_level);
|
||||
}
|
||||
|
||||
log_level_info->set_current_log_level(current_log_level);
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return log_level_info;
|
||||
}
|
||||
|
||||
unique_ptr<PbNetworkInterfacesInfo> PiscsiResponse::GetNetworkInterfacesInfo(PbResult& result) const
|
||||
{
|
||||
auto network_interfaces_info = make_unique<PbNetworkInterfacesInfo>();
|
||||
|
||||
for (const auto& network_interface : device_factory.GetNetworkInterfaces()) {
|
||||
network_interfaces_info->add_name(network_interface);
|
||||
}
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return network_interfaces_info;
|
||||
}
|
||||
|
||||
unique_ptr<PbMappingInfo> PiscsiResponse::GetMappingInfo(PbResult& result) const
|
||||
{
|
||||
auto mapping_info = make_unique<PbMappingInfo>();
|
||||
|
||||
for (const auto& [name, type] : device_factory.GetExtensionMapping()) {
|
||||
(*mapping_info->mutable_mapping())[name] = type;
|
||||
}
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return mapping_info;
|
||||
}
|
||||
|
||||
unique_ptr<PbOperationInfo> PiscsiResponse::GetOperationInfo(PbResult& result, int depth) const
|
||||
{
|
||||
auto operation_info = make_unique<PbOperationInfo>();
|
||||
|
||||
auto operation = CreateOperation(*operation_info, ATTACH, "Attach device, device-specific parameters are required");
|
||||
AddOperationParameter(*operation, "name", "Image file name in case of a mass storage device").release();
|
||||
AddOperationParameter(*operation, "interface", "Comma-separated prioritized network interface list").release();
|
||||
AddOperationParameter(*operation, "inet", "IP address and netmask of the network bridge").release();
|
||||
AddOperationParameter(*operation, "cmd", "Print command for the printer device").release();
|
||||
operation.release();
|
||||
|
||||
CreateOperation(*operation_info, DETACH, "Detach device, device-specific parameters are required").release();
|
||||
|
||||
CreateOperation(*operation_info, DETACH_ALL, "Detach all devices").release();
|
||||
|
||||
CreateOperation(*operation_info, START, "Start device, device-specific parameters are required").release();
|
||||
|
||||
CreateOperation(*operation_info, STOP, "Stop device, device-specific parameters are required").release();
|
||||
|
||||
operation = CreateOperation(*operation_info, INSERT, "Insert medium, device-specific parameters are required");
|
||||
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
|
||||
operation.release();
|
||||
|
||||
CreateOperation(*operation_info, EJECT, "Eject medium, device-specific parameters are required").release();
|
||||
|
||||
CreateOperation(*operation_info, PROTECT, "Protect medium, device-specific parameters are required").release();
|
||||
|
||||
CreateOperation(*operation_info, UNPROTECT, "Unprotect medium, device-specific parameters are required").release();
|
||||
|
||||
operation = CreateOperation(*operation_info, SERVER_INFO, "Get piscsi server information");
|
||||
if (depth) {
|
||||
AddOperationParameter(*operation, "folder_pattern", "Pattern for filtering image folder names").release();
|
||||
}
|
||||
AddOperationParameter(*operation, "file_pattern", "Pattern for filtering image file names").release();
|
||||
operation.release();
|
||||
|
||||
CreateOperation(*operation_info, VERSION_INFO, "Get piscsi server version").release();
|
||||
|
||||
CreateOperation(*operation_info, DEVICES_INFO, "Get information on attached devices").release();
|
||||
|
||||
CreateOperation(*operation_info, DEVICE_TYPES_INFO, "Get device properties by device type").release();
|
||||
|
||||
operation = CreateOperation(*operation_info, DEFAULT_IMAGE_FILES_INFO, "Get information on available image files");
|
||||
if (depth) {
|
||||
AddOperationParameter(*operation, "folder_pattern", "Pattern for filtering image folder names").release();
|
||||
}
|
||||
AddOperationParameter(*operation, "file_pattern", "Pattern for filtering image file names").release();
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, IMAGE_FILE_INFO, "Get information on image file");
|
||||
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
|
||||
operation.release();
|
||||
|
||||
CreateOperation(*operation_info, LOG_LEVEL_INFO, "Get log level information").release();
|
||||
|
||||
CreateOperation(*operation_info, NETWORK_INTERFACES_INFO, "Get the available network interfaces").release();
|
||||
|
||||
CreateOperation(*operation_info, MAPPING_INFO, "Get mapping of extensions to device types").release();
|
||||
|
||||
CreateOperation(*operation_info, RESERVED_IDS_INFO, "Get list of reserved device IDs").release();
|
||||
|
||||
operation = CreateOperation(*operation_info, DEFAULT_FOLDER, "Set default image file folder");
|
||||
AddOperationParameter(*operation, "folder", "Default image file folder name", "", true).release();
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, LOG_LEVEL, "Set log level");
|
||||
AddOperationParameter(*operation, "level", "New log level", "", true).release();
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, RESERVE_IDS, "Reserve device IDs");
|
||||
AddOperationParameter(*operation, "ids", "Comma-separated device ID list", "", true).release();
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, SHUT_DOWN, "Shut down or reboot");
|
||||
auto parameter = AddOperationParameter(*operation, "mode", "Shutdown mode", "", true).release();
|
||||
parameter->add_permitted_values("piscsi");
|
||||
// System shutdown/reboot requires root permissions
|
||||
if (!getuid()) {
|
||||
parameter->add_permitted_values("system");
|
||||
parameter->add_permitted_values("reboot");
|
||||
}
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, CREATE_IMAGE, "Create an image file");
|
||||
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
|
||||
AddOperationParameter(*operation, "size", "Image file size in bytes", "", true).release();
|
||||
parameter = AddOperationParameter(*operation, "read_only", "Read-only flag", "false").release();
|
||||
parameter->add_permitted_values("true");
|
||||
parameter->add_permitted_values("false");
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, DELETE_IMAGE, "Delete image file");
|
||||
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, RENAME_IMAGE, "Rename image file");
|
||||
AddOperationParameter(*operation, "from", "Source image file name", "", true).release();
|
||||
AddOperationParameter(*operation, "to", "Destination image file name", "", true).release();
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, COPY_IMAGE, "Copy image file");
|
||||
AddOperationParameter(*operation, "from", "Source image file name", "", true).release();
|
||||
AddOperationParameter(*operation, "to", "Destination image file name", "", true).release();
|
||||
parameter = AddOperationParameter(*operation, "read_only", "Read-only flag", "false").release();
|
||||
parameter->add_permitted_values("true");
|
||||
parameter->add_permitted_values("false");
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, PROTECT_IMAGE, "Write-protect image file");
|
||||
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, UNPROTECT_IMAGE, "Make image file writable");
|
||||
AddOperationParameter(*operation, "file", "Image file name", "", true).release();
|
||||
operation.release();
|
||||
|
||||
operation = CreateOperation(*operation_info, CHECK_AUTHENTICATION, "Check whether an authentication token is valid");
|
||||
AddOperationParameter(*operation, "token", "Authentication token to be checked", "", true).release();
|
||||
operation.release();
|
||||
|
||||
CreateOperation(*operation_info, OPERATION_INFO, "Get operation meta data").release();
|
||||
|
||||
result.set_status(true);
|
||||
|
||||
return operation_info;
|
||||
}
|
||||
|
||||
unique_ptr<PbOperationMetaData> PiscsiResponse::CreateOperation(PbOperationInfo& operation_info, const PbOperation& operation,
|
||||
const string& description) const
|
||||
{
|
||||
auto meta_data = make_unique<PbOperationMetaData>();
|
||||
meta_data->set_server_side_name(PbOperation_Name(operation));
|
||||
meta_data->set_description(description);
|
||||
int ordinal = PbOperation_descriptor()->FindValueByName(PbOperation_Name(operation))->index();
|
||||
(*operation_info.mutable_operations())[ordinal] = *meta_data.release();
|
||||
return unique_ptr<PbOperationMetaData>(&(*operation_info.mutable_operations())[ordinal]);
|
||||
}
|
||||
|
||||
unique_ptr<PbOperationParameter> PiscsiResponse::AddOperationParameter(PbOperationMetaData& meta_data,
|
||||
const string& name, const string& description, const string& default_value, bool is_mandatory) const
|
||||
{
|
||||
auto parameter = unique_ptr<PbOperationParameter>(meta_data.add_parameters());
|
||||
parameter->set_name(name);
|
||||
parameter->set_description(description);
|
||||
parameter->set_default_value(default_value);
|
||||
parameter->set_is_mandatory(is_mandatory);
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
set<id_set> PiscsiResponse::MatchDevices(const unordered_set<shared_ptr<PrimaryDevice>>& devices, PbResult& result,
|
||||
const PbCommand& command) const
|
||||
{
|
||||
set<id_set> id_sets;
|
||||
|
||||
for (const auto& device : command.devices()) {
|
||||
bool has_device = false;
|
||||
for (const auto& d : devices) {
|
||||
if (d->GetId() == device.id() && d->GetLun() == device.unit()) {
|
||||
id_sets.insert(make_pair(device.id(), device.unit()));
|
||||
has_device = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_device) {
|
||||
id_sets.clear();
|
||||
|
||||
result.set_status(false);
|
||||
result.set_msg("No device for ID " + to_string(device.id()) + ", unit " + to_string(device.unit()));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return id_sets;
|
||||
}
|
||||
|
||||
string PiscsiResponse::GetNextImageFile(const dirent *dir, const string& folder)
|
||||
{
|
||||
// Ignore unknown folder types and folder names starting with '.'
|
||||
if ((dir->d_type != DT_REG && dir->d_type != DT_DIR && dir->d_type != DT_LNK && dir->d_type != DT_BLK)
|
||||
|| dir->d_name[0] == '.') {
|
||||
return "";
|
||||
}
|
||||
|
||||
const string filename = folder + "/" + dir->d_name;
|
||||
|
||||
const bool file_exists = exists(path(filename));
|
||||
|
||||
// filesystem::file_size cannot be used here because gcc < 10.3.0 cannot handle files of more than 2 GiB
|
||||
struct stat st;
|
||||
stat(filename.c_str(), &st);
|
||||
if (dir->d_type == DT_REG && file_exists && !st.st_size) {
|
||||
LOGWARN("File '%s' in image folder '%s' is empty", dir->d_name, folder.c_str())
|
||||
return "";
|
||||
}
|
||||
|
||||
if (dir->d_type == DT_LNK && !file_exists) {
|
||||
LOGWARN("Symlink '%s' in image folder '%s' is broken", dir->d_name, folder.c_str())
|
||||
return "";
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2021-2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "devices/device_factory.h"
|
||||
#include "devices/primary_device.h"
|
||||
#include "generated/piscsi_interface.pb.h"
|
||||
#include <dirent.h>
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
using namespace piscsi_interface;
|
||||
|
||||
class PiscsiResponse
|
||||
{
|
||||
using id_set = pair<int, int>;
|
||||
|
||||
public:
|
||||
|
||||
PiscsiResponse() = default;
|
||||
~PiscsiResponse() = default;
|
||||
|
||||
bool GetImageFile(PbImageFile&, const string&, const string&) const;
|
||||
unique_ptr<PbImageFilesInfo> GetAvailableImages(PbResult&, const string&, const string&, const string&, int) const;
|
||||
unique_ptr<PbReservedIdsInfo> GetReservedIds(PbResult&, const unordered_set<int>&) const;
|
||||
void GetDevices(const unordered_set<shared_ptr<PrimaryDevice>>&, PbServerInfo&, const string&) const;
|
||||
void GetDevicesInfo(const unordered_set<shared_ptr<PrimaryDevice>>&, PbResult&, const PbCommand&, const string&) const;
|
||||
unique_ptr<PbDeviceTypesInfo> GetDeviceTypesInfo(PbResult&) const;
|
||||
unique_ptr<PbVersionInfo> GetVersionInfo(PbResult&) const;
|
||||
unique_ptr<PbServerInfo> GetServerInfo(const unordered_set<shared_ptr<PrimaryDevice>>&, PbResult&, const unordered_set<int>&,
|
||||
const string&, const string&, const string&, const string&, int) const;
|
||||
unique_ptr<PbNetworkInterfacesInfo> GetNetworkInterfacesInfo(PbResult&) const;
|
||||
unique_ptr<PbMappingInfo> GetMappingInfo(PbResult&) const;
|
||||
unique_ptr<PbLogLevelInfo> GetLogLevelInfo(PbResult&, const string&) const;
|
||||
unique_ptr<PbOperationInfo> GetOperationInfo(PbResult&, int) const;
|
||||
|
||||
private:
|
||||
|
||||
DeviceFactory device_factory;
|
||||
|
||||
const inline static array<string, 6> log_levels = { "trace", "debug", "info", "warn", "err", "off" };
|
||||
|
||||
unique_ptr<PbDeviceProperties> GetDeviceProperties(const Device&) const;
|
||||
void GetDevice(const Device&, PbDevice&, const string&) const;
|
||||
void GetAllDeviceTypeProperties(PbDeviceTypesInfo&) const;
|
||||
void GetDeviceTypeProperties(PbDeviceTypesInfo&, PbDeviceType) const;
|
||||
void GetAvailableImages(PbImageFilesInfo&, const string&, const string&, const string&, const string&, int) const;
|
||||
void GetAvailableImages(PbResult& result, PbServerInfo&, const string&, const string&, const string&, int) const;
|
||||
unique_ptr<PbOperationMetaData> CreateOperation(PbOperationInfo&, const PbOperation&, const string&) const;
|
||||
unique_ptr<PbOperationParameter> AddOperationParameter(PbOperationMetaData&, const string&, const string&,
|
||||
const string& = "", bool = false) const;
|
||||
set<id_set> MatchDevices(const unordered_set<shared_ptr<PrimaryDevice>>&, PbResult&, const PbCommand&) const;
|
||||
|
||||
static string GetNextImageFile(const dirent *, const string&);
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#include "shared/log.h"
|
||||
#include "shared/piscsi_util.h"
|
||||
#include "shared/protobuf_serializer.h"
|
||||
#include "shared/piscsi_exceptions.h"
|
||||
#include "command_context.h"
|
||||
#include "localizer.h"
|
||||
#include "piscsi_service.h"
|
||||
#include <netinet/in.h>
|
||||
#include <csignal>
|
||||
|
||||
using namespace piscsi_interface;
|
||||
using namespace piscsi_util;
|
||||
|
||||
void PiscsiService::Cleanup() const
|
||||
{
|
||||
running = false;
|
||||
|
||||
if (service_socket != -1) {
|
||||
close(service_socket);
|
||||
}
|
||||
}
|
||||
|
||||
bool PiscsiService::Init(const callback& cb, int port)
|
||||
{
|
||||
if (port <= 0 || port > 65535) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create socket for monitor
|
||||
sockaddr_in server = {};
|
||||
service_socket = socket(PF_INET, SOCK_STREAM, 0);
|
||||
if (service_socket == -1) {
|
||||
LOGERROR("Unable to create socket")
|
||||
return false;
|
||||
}
|
||||
|
||||
server.sin_family = PF_INET;
|
||||
server.sin_port = htons((uint16_t)port);
|
||||
server.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
|
||||
// Allow address reuse
|
||||
if (int yes = 1; setsockopt(service_socket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
if (bind(service_socket, (sockaddr *)&server, sizeof(sockaddr_in)) < 0) {
|
||||
cerr << "Error: Port " << port << " is in use, is piscsi or rascsi already running?" << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
execute = cb;
|
||||
|
||||
monthread = thread(&PiscsiService::Execute, this);
|
||||
monthread.detach();
|
||||
|
||||
// Interrupt handler settings
|
||||
return signal(SIGINT, KillHandler) != SIG_ERR && signal(SIGHUP, KillHandler) != SIG_ERR
|
||||
&& signal(SIGTERM, KillHandler) != SIG_ERR;
|
||||
}
|
||||
|
||||
void PiscsiService::Execute() const
|
||||
{
|
||||
#ifdef __linux__
|
||||
// Scheduler Settings
|
||||
sched_param schedparam;
|
||||
schedparam.sched_priority = 0;
|
||||
sched_setscheduler(0, SCHED_IDLE, &schedparam);
|
||||
#endif
|
||||
|
||||
// Set the affinity to a specific processor core
|
||||
FixCpu(2);
|
||||
|
||||
// Wait for the execution to start
|
||||
const timespec ts = { .tv_sec = 0, .tv_nsec = 1000};
|
||||
while (!running) {
|
||||
nanosleep(&ts, nullptr);
|
||||
}
|
||||
|
||||
// Set up the monitor socket to receive commands
|
||||
listen(service_socket, 1);
|
||||
|
||||
while (true) {
|
||||
CommandContext context;
|
||||
|
||||
try {
|
||||
PbCommand command = ReadCommand(context);
|
||||
if (context.IsValid()) {
|
||||
execute(context, command);
|
||||
}
|
||||
}
|
||||
catch(const io_exception& e) {
|
||||
LOGWARN("%s", e.what())
|
||||
|
||||
// Fall through
|
||||
}
|
||||
|
||||
context.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
PbCommand PiscsiService::ReadCommand(CommandContext& context) const
|
||||
{
|
||||
// Wait for connection
|
||||
sockaddr client = {};
|
||||
socklen_t socklen = sizeof(client);
|
||||
const int fd = accept(service_socket, &client, &socklen);
|
||||
if (fd == -1) {
|
||||
throw io_exception("accept() failed");
|
||||
}
|
||||
|
||||
PbCommand command;
|
||||
|
||||
// Read magic string
|
||||
vector<byte> magic(6);
|
||||
const size_t bytes_read = context.GetSerializer().ReadBytes(fd, magic);
|
||||
if (!bytes_read) {
|
||||
return command;
|
||||
}
|
||||
|
||||
if (bytes_read != magic.size() || memcmp(magic.data(), "RASCSI", magic.size())) {
|
||||
throw io_exception("Invalid magic");
|
||||
}
|
||||
|
||||
// Fetch the command
|
||||
context.GetSerializer().DeserializeMessage(fd, command);
|
||||
|
||||
context.SetFd(fd);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2022 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "generated/piscsi_interface.pb.h"
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
class CommandContext;
|
||||
|
||||
using namespace std;
|
||||
using namespace piscsi_interface;
|
||||
|
||||
class PiscsiService
|
||||
{
|
||||
using callback = function<bool(const CommandContext&, piscsi_interface::PbCommand&)>;
|
||||
|
||||
callback execute;
|
||||
|
||||
int service_socket = -1;
|
||||
|
||||
thread monthread;
|
||||
|
||||
static inline volatile bool running = false;
|
||||
|
||||
public:
|
||||
|
||||
PiscsiService() = default;
|
||||
~PiscsiService() = default;
|
||||
|
||||
bool Init(const callback&, int);
|
||||
void Cleanup() const;
|
||||
|
||||
bool IsRunning() const { return running; }
|
||||
void SetRunning(bool b) const { running = b; }
|
||||
|
||||
private:
|
||||
|
||||
void Execute() const;
|
||||
|
||||
PbCommand ReadCommand(CommandContext&) const;
|
||||
|
||||
static void KillHandler(int) { running = false; }
|
||||
};
|
||||
Reference in New Issue
Block a user