ssudeviceinfo.cpp 16.1 KB
Newer Older
1 2 3 4 5 6 7
/**
 * @file ssudeviceinfo.cpp
 * @copyright 2013 2013 Jolla Ltd.
 * @author Bernd Wachter <bwachter@lart.info>
 * @date 2013
 */

8

9 10 11
#include <QTextStream>
#include <QDir>

12 13 14 15
#include <QDBusMessage>
#include <QDBusConnection>
#include <QDBusArgument>

16 17
#include <sys/utsname.h>

18
#include "sandbox_p.h"
19
#include "ssudeviceinfo.h"
20
#include "ssucoreconfig.h"
21
#include "ssulog.h"
22
#include "ssuvariables.h"
23

24 25
#include "../constants.h"

26
SsuDeviceInfo::SsuDeviceInfo(QString model): QObject(){
27

28
    boardMappings = new SsuSettings(SSU_BOARD_MAPPING_CONFIGURATION, SSU_BOARD_MAPPING_CONFIGURATION_DIR);
29 30
    if (!model.isEmpty())
      cachedModel = model;
31 32
}

33 34 35
QStringList SsuDeviceInfo::adaptationRepos(){
  QStringList result;

36
  QString model = deviceVariant(true);
37 38 39

  if (boardMappings->contains(model + "/adaptation-repos"))
    result = boardMappings->value(model + "/adaptation-repos").toStringList();
40

41 42 43
  return result;
}

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
QString SsuDeviceInfo::adaptationVariables(const QString &adaptationName, QHash<QString, QString> *storageHash){
  SsuLog *ssuLog = SsuLog::instance();
  QStringList adaptationRepoList = adaptationRepos();
  // special handling for adaptation-repositories
  // - check if repo is in right format (adaptation\d*)
  // - check if the configuration has that many adaptation repos
  // - export the entry in the adaptation list as %(adaptation)
  // - look up variables for that adaptation, and export matching
  //   adaptation variable
  QRegExp regex("adaptation\\\d*", Qt::CaseSensitive, QRegExp::RegExp2);
  if (regex.exactMatch(adaptationName)){
    regex.setPattern("\\\d*");
    regex.lastIndexIn(adaptationName);
    int n = regex.cap().toInt();

    if (!adaptationRepoList.isEmpty()){
      if (adaptationRepoList.size() <= n) {
        ssuLog->print(LOG_INFO, "Note: repo index out of bounds, substituting 0" + adaptationName);
        n = 0;
      }

      QString adaptationRepo = adaptationRepoList.at(n);
      storageHash->insert("adaptation", adaptationRepo);
      ssuLog->print(LOG_DEBUG, "Found first adaptation " + adaptationName);

69
      QString model = deviceVariant(true);
70
      QHash<QString, QString> h;
71 72 73 74 75 76 77 78 79

      // add global variables for this model
      if (boardMappings->contains(model + "/variables")){
        QStringList sections = boardMappings->value(model + "/variables").toStringList();
        foreach(const QString &section, sections)
          variableSection(section, &h);
      }

      // override with variables specific to this repository
80
      variableSection(adaptationRepo, &h);
81 82 83 84 85 86 87 88 89 90 91 92 93 94

      QHash<QString, QString>::const_iterator i = h.constBegin();
      while (i != h.constEnd()){
        storageHash->insert(i.key(), i.value());
        i++;
      }
    } else
      ssuLog->print(LOG_INFO, "Note: adaptation repo for invalid repo requested " + adaptationName);

    return "adaptation";
  }
  return adaptationName;
}

95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
void SsuDeviceInfo::clearCache(){
  cachedFamily = "";
  cachedModel = "";
  cachedVariant = "";
}

bool SsuDeviceInfo::contains(const QString &model){
  QString oldModel = deviceModel();
  bool found = false;

  if (!model.isEmpty()){
    clearCache();
    setDeviceModel(model);
  }

  if (!deviceVariant(false).isEmpty())
    found = true;
  if (boardMappings->childGroups().contains(deviceModel()))
    found = true;

  if (!model.isEmpty()){
    clearCache();
    setDeviceModel(oldModel);
  }
  return found;
}

122
QString SsuDeviceInfo::deviceFamily(){
123 124 125
  if (!cachedFamily.isEmpty())
    return cachedFamily;

126
  QString model = deviceVariant(true);
127

128
  cachedFamily = "UNKNOWN";
129 130 131 132 133 134 135

  if (boardMappings->contains(model + "/family"))
    cachedFamily = boardMappings->value(model + "/family").toString();

  return cachedFamily;
}

136
QString SsuDeviceInfo::deviceVariant(bool fallback){
137 138 139 140 141 142 143 144 145
  if (!cachedVariant.isEmpty())
    return cachedVariant;

  cachedVariant = "";

  if (boardMappings->contains("variants/" + deviceModel())) {
    cachedVariant = boardMappings->value("variants/" + deviceModel()).toString();
  }

146 147 148
  if (cachedVariant == "" && fallback)
    return deviceModel();

149 150 151
  return cachedVariant;
}

152 153 154 155 156 157 158 159 160 161 162 163 164 165
QString SsuDeviceInfo::deviceModel(){
  QDir dir;
  QFile procCpuinfo;
  QStringList keys;

  if (!cachedModel.isEmpty())
    return cachedModel;

  boardMappings->beginGroup("file.exists");
  keys = boardMappings->allKeys();

  // check if the device can be identified by testing for a file
  foreach (const QString &key, keys){
    QString value = boardMappings->value(key).toString();
166
    if (dir.exists(Sandbox::map(value))){
167 168 169 170 171 172 173 174
      cachedModel = key;
      break;
    }
  }
  boardMappings->endGroup();
  if (!cachedModel.isEmpty()) return cachedModel;

  // check if the device can be identified by a string in /proc/cpuinfo
175
  procCpuinfo.setFileName(Sandbox::map("/proc/cpuinfo"));
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
  procCpuinfo.open(QIODevice::ReadOnly | QIODevice::Text);
  if (procCpuinfo.isOpen()){
    QTextStream in(&procCpuinfo);
    QString cpuinfo = in.readAll();
    boardMappings->beginGroup("cpuinfo.contains");
    keys = boardMappings->allKeys();

    foreach (const QString &key, keys){
      QString value = boardMappings->value(key).toString();
      if (cpuinfo.contains(value)){
        cachedModel = key;
        break;
      }
    }
    boardMappings->endGroup();
  }
  if (!cachedModel.isEmpty()) return cachedModel;

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
  // mer-hybris adaptations: /etc/hw-release MER_HA_DEVICE variable
  QString hwReleaseDevice = hwRelease()["MER_HA_DEVICE"];
  if (!hwReleaseDevice.isEmpty()) {
    boardMappings->beginGroup("hwrelease.device");
    keys = boardMappings->allKeys();

    foreach (const QString &key, keys) {
      QString value = boardMappings->value(key).toString();
      if (hwReleaseDevice == value) {
        cachedModel = key;
        break;
      }
    }
    boardMappings->endGroup();
  }
  if (!cachedModel.isEmpty()) return cachedModel;

211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
  // check if the device can be identified by the kernel version string
  struct utsname buf;
  if (!uname(&buf)){
    QString utsRelease(buf.release);
    boardMappings->beginGroup("uname-release.contains");
    keys = boardMappings->allKeys();

    foreach (const QString &key, keys){
      QString value = boardMappings->value(key).toString();
      if (utsRelease.contains(value)){
        cachedModel = key;
        break;
      }
    }
    boardMappings->endGroup();
  }
  if (!cachedModel.isEmpty()) return cachedModel;
228

229
  // check if there's a match on arch of generic fallback. This probably
230 231 232 233
  // only makes sense for x86
  boardMappings->beginGroup("arch.equals");
  keys = boardMappings->allKeys();

234
  SsuCoreConfig *settings = SsuCoreConfig::instance();
235 236
  foreach (const QString &key, keys){
    QString value = boardMappings->value(key).toString();
237
    if (settings->value("arch").toString() == value){
238 239 240 241 242 243 244 245 246 247
      cachedModel = key;
      break;
    }
  }
  boardMappings->endGroup();
  if (cachedModel.isEmpty()) cachedModel = "UNKNOWN";

  return cachedModel;
}

248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
static QStringList
ofonoGetImeis()
{
    QStringList result;

    QDBusMessage reply = QDBusConnection::systemBus().call(
            QDBusMessage::createMethodCall("org.ofono", "/",
                "org.ofono.Manager", "GetModems"));

    foreach (const QVariant &v, reply.arguments()) {
        if (v.canConvert<QDBusArgument>()) {
            const QDBusArgument arg = v.value<QDBusArgument>();
            if (arg.currentType() == QDBusArgument::ArrayType) {
                arg.beginArray();
                while (!arg.atEnd()) {
                    if (arg.currentType() == QDBusArgument::StructureType) {
                        QString path;
                        QVariantMap props;

                        arg.beginStructure();
                        arg >> path >> props;
                        arg.endStructure();

                        if (props.contains("Serial")) {
                            result << props["Serial"].toString();
                        }
                    }
                }
                arg.endArray();
            }
        }
    }
280

281 282 283
    return result;
}

284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
static QStringList
getWlanMacs()
{
    // Based on QtSystems' qnetworkinfo_linux.cpp
    QStringList result;

    QStringList dirs = QDir(QLatin1String("/sys/class/net/"))
            .entryList(QStringList() << QLatin1String("wlan*"));
    foreach (const QString &dir, dirs) {
        QFile carrier(QString("/sys/class/net/%1/address").arg(dir));
        if (carrier.open(QIODevice::ReadOnly)) {
            result.append(QString::fromLatin1(carrier.readAll().simplified().data()));
        }
    }
    return result;
}

static QString
normalizeUid(const QString& uid)
{
    // Normalize by stripping colons, dashes and making it lowercase
    return uid.trimmed().replace(":", "").replace("-", "").toLower();
}

308
QString SsuDeviceInfo::deviceUid(){
309
  SsuLog *ssuLog = SsuLog::instance();
310
  QStringList imeis = ofonoGetImeis();
311 312 313
  if (imeis.size() > 0) {
      return imeis[0];
  }
314

315 316 317 318
  QStringList wlanMacs = getWlanMacs();
  if (wlanMacs.size() > 0) {
      return normalizeUid(wlanMacs[0]);
  }
319

320
  ssuLog->print(LOG_WARNING, "Could not get IMEI(s) from ofono, nor WLAN mac, trying fallback");
321

322 323 324 325 326 327
  // The fallback list is taken from QtSystems' qdeviceinfo_linux.cpp
  QStringList fallbackFiles;
  fallbackFiles << "/sys/devices/virtual/dmi/id/product_uuid";
  fallbackFiles << "/etc/machine-id";
  fallbackFiles << "/etc/unique-id";
  fallbackFiles << "/var/lib/dbus/machine-id";
328

329 330 331 332 333
  foreach (const QString &filename, fallbackFiles) {
      QFile machineId(filename);
      if (machineId.open(QFile::ReadOnly | QFile::Text)) {
          QTextStream in(&machineId);
          return normalizeUid(in.readAll());
334
      }
335 336
  }

337 338
  ssuLog->print(LOG_CRIT, "Could not read fallback UID - returning empty string");
  return "";
339
}
340

341 342 343
QStringList SsuDeviceInfo::disabledRepos(){
  QStringList result;

344
  QString model = deviceVariant(true);
345 346 347 348 349 350 351

  if (boardMappings->contains(model + "/disabled-repos"))
    result = boardMappings->value(model + "/disabled-repos").toStringList();

  return result;
}

352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
QString SsuDeviceInfo::displayName(const int type){
  QString model = deviceModel();
  QString variant = deviceVariant(false);
  QString value, key;


  switch (type){
    case Ssu::DeviceManufacturer:
      key = "/deviceManufacturer";
      break;
    case Ssu::DeviceModel:
      key = "/prettyModel";
      break;
    case Ssu::DeviceDesignation:
      key = "/deviceDesignation";
      break;
    default:
      return "";
  }

  /*
   * Go through different levels of fallbacks:
   * 1. model specific setting
   * 2. variant specific setting
   * 3. global setting
   * 4. return model name, or "UNKNOWN" in case query was for manufacturer
   */

  if (boardMappings->contains(model + key))
    value = boardMappings->value(model + key).toString();
  else if (variant != "" && boardMappings->contains(variant + key))
    value = boardMappings->value(variant + key).toString();
  else if (boardMappings->contains(key))
    value = boardMappings->value(key).toString();
  else if (type != Ssu::DeviceManufacturer)
    value = model;
  else
    value = "UNKNOWN";

  return value;
}

Aard's avatar
Aard committed
394 395 396 397
// this half belongs into repo-manager, as it not only handles board-specific
// repositories. Right now this one looks like the better place due to the
// connection to board specific stuff, though
QStringList SsuDeviceInfo::repos(bool rnd, int filter){
398 399 400
  int adaptationCount = adaptationRepos().size();
  QStringList result;

401 402 403 404 405

  ///@TODO move this to a hash, containing repo and enabled|disabled
  ///      write repos with enabled/disabled to disks
  ///      for the compat functions providing a stringlist, do the filtering
  ///      run only when creating the list, based on the enabled|disabled flags
Aard's avatar
Aard committed
406 407 408
  if (filter == Ssu::NoFilter ||
      filter == Ssu::BoardFilter ||
      filter == Ssu::BoardFilterUserBlacklist){
409 410 411 412
    // for repo names we have adaptation0, adaptation1, ..., adaptationN
    for (int i=0; i<adaptationCount; i++)
      result.append(QString("adaptation%1").arg(i));

Aard's avatar
Aard committed
413
    // now read the release/rnd repos
414
    SsuSettings repoSettings(SSU_REPO_CONFIGURATION, QSettings::IniFormat);
Aard's avatar
Aard committed
415 416 417
    QString repoKey = (rnd ? "default-repos/rnd" : "default-repos/release");
    if (repoSettings.contains(repoKey))
      result.append(repoSettings.value(repoKey).toStringList());
418

Aard's avatar
Aard committed
419
    // TODO: add specific repos (developer, sdk, ..)
420

Aard's avatar
Aard committed
421 422 423 424
    // add device configured repos
    if (boardMappings->contains(deviceVariant(true) + "/repos"))
      result.append(boardMappings->value(deviceVariant(true) + "/repos").toStringList());

425 426 427 428 429
    // add device configured repos only valid for rnd and/or release
    repoKey = (rnd ? "/repos-rnd" : "/repos-release");
    if (boardMappings->contains(deviceVariant(true) + repoKey))
      result.append(boardMappings->value(deviceVariant(true) + repoKey).toStringList());

Aard's avatar
Aard committed
430 431 432 433 434
    // read the disabled repositories for this device
    // user can override repositories disabled here in the user configuration
    foreach (const QString &key, disabledRepos())
      result.removeAll(key);
  }
435

Aard's avatar
Aard committed
436
  result.removeDuplicates();
437 438 439
  return result;
}

440
QVariant SsuDeviceInfo::variable(QString section, const QString &key){
441 442 443 444 445
  /// @todo compat-setting as ssudeviceinfo guaranteed to prepend sections with var-;
  ///       SsuVariables does not have this guarantee. Remove from here as well.
  if (!section.startsWith("var-"))
    section = "var-" + section;

446
  return SsuVariables::variable(boardMappings, section, key);
447 448
}

449
void SsuDeviceInfo::variableSection(QString section, QHash<QString, QString> *storageHash){
450 451 452
  if (!section.startsWith("var-"))
    section = "var-" + section;

453
  SsuVariables::variableSection(boardMappings, section, storageHash);
454 455 456 457 458 459 460 461 462 463 464
}

void SsuDeviceInfo::setDeviceModel(QString model){
  if (model == "")
    cachedModel = "";
  else
    cachedModel = model;

  cachedFamily = "";
  cachedVariant = "";
}
465 466

QVariant SsuDeviceInfo::value(const QString &key, const QVariant &value){
467
  if (boardMappings->contains(deviceModel()+"/"+key)){
468
    return boardMappings->value(deviceModel()+"/"+key);
469 470
  } else if (boardMappings->contains(deviceVariant()+"/"+key)){
    return boardMappings->value(deviceVariant()+"/"+key);
471 472 473 474
  }

  return value;
}
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545

QMap<QString, QString> SsuDeviceInfo::hwRelease()
{
  QMap<QString, QString> result;

  // Specification of the format, encoding is similar to /etc/os-release
  // http://www.freedesktop.org/software/systemd/man/os-release.html

  QFile hwRelease("/etc/hw-release");
  if (hwRelease.open(QIODevice::ReadOnly | QIODevice::Text)) {
    QTextStream in(&hwRelease);

    // "All strings should be in UTF-8 format, and non-printable characters
    // should not be used."
    in.setCodec("UTF-8");

    while (!in.atEnd()) {
      QString line = in.readLine();

      // "Lines beginning with "#" shall be ignored as comments."
      if (line.startsWith('#')) {
        continue;
      }

      QString key = line.section('=', 0, 0);
      QString value = line.section('=', 1);

      // Remove trailing whitespace in value
      value = value.trimmed();

      // POSIX.1-2001 says uppercase, digits and underscores.
      //
      // Bash uses "[a-zA-Z_]+[a-zA-Z0-9_]*", so we'll use that too,
      // as we can safely assume that "shell-compatible variable
      // assignments" means it should be compatible with bash.
      //
      // see http://stackoverflow.com/a/2821183
      // and http://stackoverflow.com/a/2821201
      if (!QRegExp("[a-zA-Z_]+[a-zA-Z0-9_]*").exactMatch(key)) {
        qWarning("Invalid key in input line: '%s'", qPrintable(line));
        continue;
      }

      // "Variable assignment values should be enclosed in double or
      // single quotes if they include spaces, semicolons or other
      // special characters outside of A-Z, a-z, 0-9."
      if (((value.at(0) == '\'') || (value.at(0) == '"'))) {
        if (value.at(0) != value.at(value.size() - 1)) {
          qWarning("Quoting error in input line: '%s'", qPrintable(line));
          continue;
        }

        // Remove the quotes
        value = value.mid(1, value.size() - 2);
      }

      // "If double or single quotes or backslashes are to be used within
      // variable assignments, they should be escaped with backslashes,
      // following shell style."
      value = value.replace("\\\"", "\"");
      value = value.replace("\\'", "'");
      value = value.replace("\\\\", "\\");

      result[key] = value;
    }

    hwRelease.close();
  }

  return result;
}