• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 30213925900

26 Jul 2026 06:03PM UTC coverage: 15.298% (-0.01%) from 15.311%
30213925900

push

github

jcelerier
3rdparty: bump libossia for the network-context poll fix

Brings in ossia/libossia#913: SDL joystick init no longer requires haptic
support (which the Emscripten SDL2 port does not have), and
network_context::poll() restarts the io_context, without which the
WebAssembly polling path stops after its first tick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019oPj1zRcxSQX7FNni7EHM6

30400 of 198713 relevant lines covered (15.3%)

997.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.0
/src/plugins/score-plugin-packagemanager/PackageManager/Model.cpp
1
// This is an open source non-commercial project. Dear PVS-Studio, please check
2
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
#include "Model.hpp"
4

5
#include <Library/LibrarySettings.hpp>
6

7
#include <score/application/ApplicationContext.hpp>
8
#include <score/application/GUIApplicationContext.hpp>
9
#include <score/plugins/settingsdelegate/SettingsDelegateModel.hpp>
10
#include <score/widgets/MessageBox.hpp>
11

12
#include <core/application/ApplicationSettings.hpp>
13

14
#include <ossia/detail/algorithms.hpp>
15

16
#include <QApplication>
17
#include <QDebug>
18
#include <QDir>
19
#include <QFile>
20
#include <QJsonArray>
21
#include <QJsonDocument>
22
#include <QJsonObject>
23
#include <QNetworkReply>
24
#include <QSet>
25
#include <QSettings>
26
#include <QStandardItemModel>
27
#include <QStringList>
28
#include <QTimer>
29
#include <QVariant>
30
#include <qmessagebox.h>
31

32
#include <PackageManager/FileDownloader.hpp>
33

34
#include <score_git_info.hpp>
35
#include <wobjectimpl.h>
36
#include <zipdownloader.hpp>
37
W_OBJECT_IMPL(PM::PluginSettingsModel)
4✔
38
namespace PM
39
{
40
static int64_t sizeToInt(QString s) noexcept
×
41
{
42
  int64_t mult = 1;
×
43
  if(s.endsWith('K', Qt::CaseInsensitive)) {
×
44
    mult = 1024;
×
45
    s.resize(s.size() - 1);
×
46
  }
×
47
  else if(s.endsWith('M', Qt::CaseInsensitive)) {
×
48
    mult = 1024 * 1024;
×
49
    s.resize(s.size() - 1);
×
50
  }
×
51
  else if(s.endsWith('G', Qt::CaseInsensitive)) {
×
52
    mult = 1024 * 1024 * 1024;
×
53
    s.resize(s.size() - 1);
×
54
  }
×
55
  return mult * s.trimmed().toDouble();
×
56
}
57

58
PluginSettingsModel::PluginSettingsModel(
72✔
59
    const UuidKey<score::SettingsDelegateFactory>& k, QSettings& set,
60
    const score::ApplicationContext& ctx)
61
    : score::SettingsDelegateModel{k, nullptr}
36✔
62
    , localPlugins{ctx}
36✔
63
    , remoteSelection{&remotePlugins}
36✔
64
{
36✔
65
  connect(
36✔
66
      &mgr, &QNetworkAccessManager::finished, this, &PluginSettingsModel::on_message);
36✔
67

68
  if(ctx.applicationSettings.gui)
36✔
69
    QTimer::singleShot(3000, this, &PluginSettingsModel::refresh);
36✔
70
}
×
71

72
PluginSettingsModel::~PluginSettingsModel() { }
72✔
73

74
void PluginSettingsModel::refresh()
×
75
{
76
  if(qEnvironmentVariableIsSet("SCORE_SANITIZE_SKIP_CHECKS"))
×
77
    return;
×
78
  QNetworkRequest rqst{QUrl(
×
79
      "https://raw.githubusercontent.com/ossia/score-packages/refs/heads/"
×
80
      "master/addons.json")};
81
  mgr.get(rqst);
×
82
}
×
83

84
void PluginSettingsModel::handleAddonList(const QJsonObject& obj)
×
85
{
86
  show_progress();
×
87
  auto arr = obj["addons"].toArray();
×
88
  m_addonsToRetrieve = arr.size();
×
89
  int delay = 0;
×
90
  for(QJsonValue elt : arr)
×
91
  {
92
    QTimer::singleShot(
×
93
        delay, this, [this, url = QUrl(elt.toString())] { requestInformation(url); });
×
94
    delay += 16;
×
95
  }
×
96
}
×
97

98
void PluginSettingsModel::handleAddon(const QJsonObject& obj)
×
99
{
100
  RemotePackagesModel* model = &remotePlugins;
×
101

102
  if(m_addonsToRetrieve == (std::ssize(model->m_vec) + 1) || m_addonsToRetrieve <= 0)
×
103
    reset_progress();
×
104
  else
105
    update_progress(double(std::ssize(model->m_vec) + 1) / m_addonsToRetrieve);
×
106

107
  auto addon = Package::fromJson(obj);
×
108
  if(!addon)
×
109
    return;
×
110

111
  auto& add = *addon;
×
112

113
  // Load images
114
  if(!add.smallImagePath.isEmpty())
×
115
  {
116
    // c.f. https://wiki.qt.io/Download_Data_from_URL
117
    auto dl = new score::FileDownloader{QUrl{add.smallImagePath}};
×
118
    connect(dl, &score::FileDownloader::downloaded, this, [=](QByteArray arr) {
×
119
      model->updateAddon(
×
120
          add.key, [=](Package& add) { add.smallImage.loadFromData(arr); });
×
121

122
      dl->deleteLater();
×
123
    });
×
124
  }
×
125

126
  if(!add.largeImagePath.isEmpty())
×
127
  {
128
    // c.f. https://wiki.qt.io/Download_Data_from_URL
129
    auto dl = new score::FileDownloader{QUrl{add.largeImagePath}};
×
130
    connect(dl, &score::FileDownloader::downloaded, this, [=](QByteArray arr) {
×
131
      model->updateAddon(
×
132
          add.key, [=](Package& add) { add.largeImage.loadFromData(arr); });
×
133

134
      dl->deleteLater();
×
135
    });
×
136
  }
×
137

138
  model->addAddon(std::move(add));
×
139
}
×
140

141
void PluginSettingsModel::on_message(QNetworkReply* rep)
×
142
{
143
  auto res = rep->readAll();
×
144
  auto json = QJsonDocument::fromJson(res).object();
×
145

146
  if(json.contains("addons"))
×
147
  {
148
    handleAddonList(json);
×
149
  }
×
150
  else if(json.contains("name"))
×
151
  {
152
    handleAddon(json);
×
153
  }
×
154
  else
155
  {
156
    qDebug() << rep->request().url().toString() << ' ' << res;
×
157
    reset_progress();
×
158
  }
159
  rep->deleteLater();
×
160

161
  if(!m_firstTimeCheck)
×
162
  {
163
    m_firstTimeCheck = true;
×
164
    QTimer::singleShot(1000, this, &PluginSettingsModel::firstTimeLibraryDownload);
×
165
  }
×
166
}
×
167

168
void PluginSettingsModel::firstTimeLibraryDownload()
×
169
{
170
  // Never block a non-interactive session (tests, CI, headless) on a modal
171
  // question; same escape hatch as refresh().
172
  if(qEnvironmentVariableIsSet("SCORE_SANITIZE_SKIP_CHECKS"))
×
173
    return;
×
174

175
  const auto& lib = score::GUIAppContext().settings<Library::Settings::Model>();
×
176
  const QString lib_folder = lib.getPackagesPath() + "/default";
×
177
  const QString lib_info = lib_folder + "/package.json";
×
178
  if(QFile file{lib_info}; !file.exists())
×
179
  {
180
    auto dl = score::question(
×
181
        qApp->activeWindow(), tr("Download the user library ?"),
×
182
        tr("The user library has not been found. \n"
×
183
           "Do you want to download it from the internet ? \n\n"
184
           "Note: you can always download it later from : \n"
185
           "https://github.com/ossia/score-user-library"));
186

187
    if(dl == QMessageBox::Yes)
×
188
    {
189
      zdl::download_and_extract(
×
190
          QUrl{"https://github.com/ossia/score-user-library/archive/master.zip"},
×
191
          lib.getPackagesPath(), [](const auto&) mutable {
×
192
        auto& lib = score::GUIAppContext().settings<Library::Settings::Model>();
×
193
        QDir packages_dir{lib.getPackagesPath()};
×
194
        packages_dir.rename("score-user-library-master", "default");
×
195

196
        lib.rescanLibrary();
×
197
      }, [](qint64 bytesReceived, qint64 bytesTotal) {
×
198
        qDebug() << (((bytesReceived / 1024.) / (bytesTotal / 1024.)) * 100)
×
199
                 << "% downloaded";
×
200
      }, [] {});
×
201
    }
×
202
  }
×
203
  else
204
  {
205
    checkAll();
×
206
  }
207
}
×
208

209
void PluginSettingsModel::checkAll()
×
210
{
211
  auto local_model = &localPlugins;
×
212
  auto remote_model = &remotePlugins;
×
213

214
  std::vector<Package*> to_update;
×
215
  for(auto& addon : local_model->addons())
×
216
  {
217
    auto key = addon.key;
×
218
    auto it = ossia::find_if(
×
219
        remote_model->addons(), [&](auto& pkg) { return pkg.key == addon.key; });
×
220
    if(it == remote_model->addons().end())
×
221
    {
222
      qDebug() << "Addon " << addon.name << "not found on the server!";
×
223
      continue;
×
224
    }
225

226
    if(it->version <= addon.version)
×
227
      continue;
×
228

229
    to_update.push_back(&*it);
×
230
  }
231

232
  if(!to_update.empty())
×
233
  {
234
    QString s = tr("Some installed packages are out-of-date: \n");
×
235
    for(auto pkg : to_update)
×
236
    {
237
      s += tr("- %1 (version %3)\n").arg(pkg->name).arg(pkg->version);
×
238
    }
239
    s += tr("Head to Settings > Packages to update them");
×
240
    score::information(qApp->activeWindow(), tr("Packages can be updated"), s);
×
241
  }
×
242
}
×
243

244
void PluginSettingsModel::requestInformation(QUrl url)
×
245
{
246
  QNetworkRequest rqst{url};
×
247
  mgr.get(rqst);
×
248
}
×
249

250
void PluginSettingsModel::installAddon(const Package& addon)
×
251
{
252
  if(addon.files.empty())
×
253
  {
254
    reset_progress();
×
255
    return;
×
256
  }
257

258
  const auto& lib = score::AppContext().settings<Library::Settings::Model>();
×
259
  const QString& installPath
×
260
      = addon.kind == "support" ? lib.getSupportPath() : lib.getPackagesPath();
×
261
  auto sz_bytes = sizeToInt(addon.size);
×
262

263
  for(auto f : addon.files)
×
264
    zdl::download_and_extract(
×
265
        f, QDir{installPath}.absolutePath(),
×
266
        [this, installPath, addon](const std::vector<QString>& res) {
×
267
      reset_progress();
×
268
      if(res.empty())
×
269
        return;
×
270
      // We want the extracted folder to have the name of the addon
271
      {
272
        QDir addons_dir{installPath};
×
273
        QFileInfo a_file(res[0]);
×
274
        auto d = a_file.dir();
×
275
        auto old_d = d;
×
276
        while(d.cdUp() && !d.isRoot())
×
277
        {
278
          if(d == addons_dir)
×
279
          {
280
            addons_dir.rename(old_d.dirName(), addon.raw_name);
×
281
            break;
×
282
          }
283
          old_d = d;
×
284
        }
285
      }
×
286

287
      information(
×
288
          tr("Addon downloaded"),
×
289
          tr("The addon %1 has been successfully installed in :\n"
×
290
             "%2\n\n"
291
             "It will be built and enabled shortly.\nCheck the message "
292
             "console for errors if nothing happens.")
293
              .arg(addon.name)
×
294
              .arg(QFileInfo(installPath).absoluteFilePath()));
×
295
    }, [this, sz_bytes](qint64 received, qint64 total) {
×
296
      if(total < received)
×
297
        total = sz_bytes;
×
298
      progress_from_bytes(received, total); },
×
299
        [this, addon] {
×
300
      reset_progress();
×
301
      warning(
×
302
          tr("Download failed"),
×
303
          tr("The package %1 could not be downloaded.").arg(addon.name));
×
304
    });
×
305
}
×
306

307
void PluginSettingsModel::installSDK()
×
308
{
309
  auto platform = score::addonArchitecture();
×
310

311
  const QString sdk_path{
×
312
      score::AppContext().settings<Library::Settings::Model>().getSDKPath() + '/'
×
313
      + SCORE_TAG_NO_V};
×
314
  QDir{}.mkpath(sdk_path);
×
315

316
  const QUrl sdk_url
317
      = QString("https://github.com/ossia/score/releases/download/%1/sdk-%2.zip")
×
318
            .arg(SCORE_TAG)
×
319
            .arg(platform);
×
320

321
  zdl::download_and_extract(
×
322
      sdk_url, QFileInfo{sdk_path}.absoluteFilePath(),
×
323
      [this](const std::vector<QString>& res) {
×
324
    reset_progress();
×
325
    if(res.empty())
×
326
      return;
×
327
    information(
×
328
        tr("SDK downloaded"),
×
329
        tr("The SDK has been successfully installed in the user library."));
×
330

331
    set_info();
×
332
  }, [this](qint64 received, qint64 total) { progress_from_bytes(received, total); },
×
333
      [this] {
×
334
    reset_progress();
×
335
    warning(tr("Download failed"), tr("The SDK could not be downloaded."));
×
336
  });
×
337
}
×
338

339
void PluginSettingsModel::installLibrary(const Package& addon)
×
340
{
341
  const auto& lib = score::AppContext().settings<Library::Settings::Model>();
×
342
  const QString& installPath
×
343
      = addon.kind == "support" ? lib.getSupportPath() : lib.getPackagesPath();
×
344
  const QString destination{installPath + "/" + addon.raw_name};
×
345

346
  if(QDir dest{destination}; dest.exists())
×
347
    dest.removeRecursively();
×
348

349
  QDir{}.mkpath(destination);
×
350

351
  auto sz_bytes = sizeToInt(addon.size);
×
352
  for(auto f : addon.files)
×
353
    zdl::download_and_extract(
×
354
        f, QFileInfo{destination}.absoluteFilePath(),
×
355
        [this, addon, destination](const std::vector<QString>& res) {
×
356
      on_packageInstallSuccess(addon, destination, res);
×
357
    }, [this, sz_bytes](qint64 received, qint64 total) {
×
358
      if(total < received)
×
359
        total = sz_bytes;
×
360
      progress_from_bytes(received, total);
×
361
    },
×
362
        [this, addon] { on_packageInstallFailure(addon); });
×
363
}
×
364

365
void PluginSettingsModel::on_packageInstallSuccess(
×
366
    const Package& addon, const QDir& destination, const std::vector<QString>& res)
367
{
368
  reset_progress();
×
369
  if(res.empty())
×
370
    return;
×
371

372
  // Often zip files contain a single, empty directory.
373
  // In that case, we move everything up a level to make the library cleaner.
374
  QDir dir{destination};
×
375
  auto files = dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
×
376
  if(files.size() == 1)
×
377
  {
378
    auto child = files[0];
×
379
    QFileInfo info{dir.absoluteFilePath(child)};
×
380
    if(info.isDir())
×
381
    {
382
      dir.rename(child, "___score_tmp___");
×
383
      QDir subdir{dir.absoluteFilePath("___score_tmp___")};
×
384

385
      for(auto& entry :
×
386
          subdir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot))
×
387
      {
388
        dir.rename(
×
389
            QString{"___score_tmp___%1%2"}.arg(QDir::separator()).arg(entry), entry);
×
390
      }
391

392
      subdir.removeRecursively();
×
393
    }
×
394
  }
×
395

396
  {
397
    QFile f{dir.absoluteFilePath("package.json")};
×
398
    if(f.open(QIODevice::WriteOnly))
×
399
    {
400
      QJsonObject obj;
×
401
      obj["name"] = addon.name;
×
402
      obj["raw_name"] = addon.raw_name;
×
403
      obj["version"] = addon.version;
×
404
      obj["kind"] = addon.kind;
×
405
      obj["url"] = addon.url;
×
406
      obj["short"] = addon.shortDescription;
×
407
      obj["long"] = addon.longDescription;
×
408
      obj["size"] = addon.size;
×
409
      // TODO images
410
      obj["key"] = QString{score::uuids::toByteArray(addon.key.impl())};
×
411

412
      f.write(QJsonDocument{obj}.toJson());
×
413
    }
×
414
  }
×
415

416
  information(
×
417
      tr("Package downloaded"),
×
418
      tr("The package %1 has been successfully installed in the user library.")
×
419
          .arg(addon.name));
×
420

421
  localPlugins.registerAddon(dir.absolutePath());
×
422
  set_info();
×
423
}
×
424

425
void PluginSettingsModel::on_packageInstallFailure(const Package& addon)
×
426
{
427
  reset_progress();
×
428
  warning(
×
429
      tr("Download failed"),
×
430
      tr("The package %1 could not be downloaded.").arg(addon.name));
×
431
}
×
432
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc