Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ab35783
tests: pin what happens when a factory is missing
jcelerier Aug 6, 2026
d9fd5b8
device: stop destroying the settings of an unavailable protocol
jcelerier Aug 6, 2026
33b4a9e
network: survive a peer whose build differs from ours
jcelerier Aug 6, 2026
715d10e
tests: drop stale doc references from the heterogeneous-build suite
jcelerier Aug 6, 2026
3b469ff
process: keep a process whose plug-in is missing instead of dropping it
jcelerier Aug 6, 2026
ebab03c
process: give a stand-in process a layer, without touching the rest
jcelerier Aug 6, 2026
3fc0a4b
process: keep a port whose plug-in is missing instead of aborting
jcelerier Aug 6, 2026
2dbb78a
document: open a document that names a plug-in we do not have
jcelerier Aug 6, 2026
63c5473
tests: cover the binary path for a port with no factory
jcelerier Aug 6, 2026
881db08
remotecontrol: require a token, and stop listening when switched off
jcelerier Aug 6, 2026
5d1ae65
tests: let a test declared inside src/ use the app fixtures
jcelerier Aug 6, 2026
e084996
deviceexplorer: give up on a device that never connects
jcelerier Aug 6, 2026
7b08625
core: name the path schemes, and stop relativizing into the wrong folder
jcelerier Aug 6, 2026
a666b6d
core: one place to ask the user for a file
jcelerier Aug 6, 2026
5c43003
core: keep a document plug-in whose factory is missing
jcelerier Aug 6, 2026
db56123
curve: say why a segment of unknown type is not preserved
jcelerier Aug 6, 2026
1f6c745
core: let something enumerate the commands a build has
jcelerier Aug 6, 2026
c62f08d
core: separate asking for the files of a score from answering
jcelerier Aug 6, 2026
9b13842
core: a document says where its files are
jcelerier Aug 6, 2026
d15a84f
core: create a document's environment before anything asks for it
jcelerier Aug 6, 2026
7095ac2
core: a preserved payload remembers which format it is in
jcelerier Aug 6, 2026
673a72c
core: a path checks what it found is the type it asked for
jcelerier Aug 6, 2026
44b8db4
remotecontrol: do not delete sockets while iterating the list they er…
jcelerier Aug 6, 2026
075bb3b
core: keep a reference that cannot be placed instead of erasing it
jcelerier Aug 6, 2026
7639611
remotecontrol: let the shipped remote send a token, and say when it c…
jcelerier Aug 6, 2026
4a2de6e
scenario: do not look up a factory for a process that has none
jcelerier Aug 6, 2026
29ba3a8
explorer: survive a device whose protocol this build does not have
jcelerier Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions cmake/ScoreTests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,14 @@ function(score_add_test NAME)
${ARG_LIBS}
${QT_PREFIX}::Core)

# The app/document fixtures library is defined late (tests/fixtures, after
# src/). Per-plugin unit tests built during src/ are app-free and don't need
# it, so only link it when it already exists.
# The fixtures target is defined late (tests/fixtures, after src/), so tests
# declared from inside src/ -- plug-ins and add-ons -- cannot link it yet. It
# is header-only, so hand those the include path instead, which is what the
# else branch below is for.
if(TARGET score_test_fixtures AND NOT ARG_STANDALONE)
target_link_libraries(${NAME} PRIVATE score_test_fixtures)
else()
target_include_directories(${NAME} PRIVATE "${SCORE_ROOT_SOURCE_DIR}/tests/fixtures")
endif()

if(ARG_GUI OR ARG_APP)
Expand Down
8 changes: 8 additions & 0 deletions src/lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ set(HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/CommonTypes.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/JSONValueVisitor.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/JSONVisitor.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/OpaquePayload.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/MapSerialization.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/MimeVisitor.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/StdVariantSerialization.hpp"
Expand All @@ -229,6 +230,8 @@ set(HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Cursor.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/DeleteAll.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/File.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Uri.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Environment.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/FindStringInFile.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/IdentifierGeneration.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/MapCopy.hpp"
Expand Down Expand Up @@ -328,6 +331,7 @@ set(HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/DoubleSpinBox.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/IconProvider.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/IntSlider.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/FileDialog.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/FormWidget.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/ItemViewDrag.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/Layout.hpp"
Expand Down Expand Up @@ -447,6 +451,7 @@ set(SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/score/selection/SelectionStack.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/DataStreamVisitor.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/JSONObjectVisitor.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/OpaquePayload.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/serialization/QtTypesJsonVisitors.cpp"

"${CMAKE_CURRENT_SOURCE_DIR}/score/model/path/ObjectIdentifierSerialization.cpp"
Expand All @@ -463,6 +468,8 @@ set(SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/std/String.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Cuda.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/File.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Uri.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Environment.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/FileContains.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/FileWatch.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/ProjectFiles.cpp"
Expand Down Expand Up @@ -523,6 +530,7 @@ set(SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/ControlWidgets.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/DoubleSlider.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/DoubleSpinBox.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/FileDialog.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/FormWidget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/IconProvider.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/IntSlider.cpp"
Expand Down
18 changes: 18 additions & 0 deletions src/lib/core/document/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ const std::vector<DocumentPlugin*>& DocumentContext::pluginModels() const
return document.model().pluginModels();
}

score::Environment& DocumentContext::environment() const noexcept
{
return document.environment();
}

Document::Document(
const QString& name, const Id<DocumentModel>& id, DocumentDelegateFactory& factory,
QWidget* parentview, QObject* parent)
Expand Down Expand Up @@ -156,6 +161,19 @@ Document::Document(
// this, &Document::fileNameChanged);
}

score::Environment& Document::environment() const noexcept
{
if(!m_environment)
m_environment = std::make_unique<score::LocalEnvironment>(m_context);
return *m_environment;
}

void Document::setEnvironment(std::unique_ptr<score::Environment> env)
{
SCORE_ASSERT(env);
m_environment = std::move(env);
}

void Document::init()
{
if(this->m_context.app.applicationSettings.gui)
Expand Down
13 changes: 13 additions & 0 deletions src/lib/core/document/Document.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include <score/document/DocumentContext.hpp>
#include <score/tools/Environment.hpp>
#include <score/locking/ObjectLocker.hpp>
#include <score/selection/FocusManager.hpp>
#include <score/selection/SelectionStack.hpp>
Expand All @@ -15,6 +16,7 @@
#include <QTimer>
#include <QVariant>

#include <memory>
#include <verdigris>

class QObject;
Expand Down Expand Up @@ -73,6 +75,16 @@ class SCORE_LIB_BASE_EXPORT Document final : public QObject

const DocumentContext& context() const noexcept { return m_context; }

//! Where this document's files are. Local unless something says otherwise.
//!
//! Created on demand rather than in init(): deserialization resolves paths,
//! and it runs from the constructors before init() would have had a chance.
score::Environment& environment() const noexcept;

//! Take the files of this document to be somewhere else -- another machine,
//! typically, once it is being edited through a session.
void setEnvironment(std::unique_ptr<score::Environment> env);

DocumentModel& model() const noexcept { return *m_model; }

DocumentPresenter* presenter() const noexcept { return m_presenter; }
Expand Down Expand Up @@ -150,6 +162,7 @@ class SCORE_LIB_BASE_EXPORT Document final : public QObject
DocumentBackupManager* m_backupMgr{};

DocumentContext m_context;
mutable std::unique_ptr<score::Environment> m_environment;

std::optional<score::RestorableDocument> m_initialData{};
bool m_virgin{false}; // Used to check if we can safely close it
Expand Down
17 changes: 13 additions & 4 deletions src/lib/core/document/DocumentSerialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,22 @@ void Document::loadModel(
}
case JSONObject::type(): {
auto doc = readJson(data);
bool ok = DocumentManager::checkAndUpdateJson(doc, m_context.app);
if(!ok)
const auto check = DocumentManager::checkAndUpdateJson(doc, m_context.app);
if(!check.loadable)
{
throw std::runtime_error(
"The save format is too old. Wait until the developers implement "
"loading of the older save format.");
"This document was written by a newer version of score, or by one "
"with a newer version of a plug-in you have, and cannot be read.");
}

for(const auto& plugin : check.missingPlugins)
{
qWarning() << "Opening a document that uses plug-in"
<< score::uuids::toByteArray(plugin.impl())
<< "which this build does not have. What it contributed is "
"kept as-is and will be written back unchanged.";
}

m_model->loadDocumentAsJson(m_context, doc, factory);
break;
}
Expand Down
26 changes: 16 additions & 10 deletions src/lib/core/presenter/DocumentManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -668,11 +668,12 @@ bool DocumentManager::preparingNewDocument() const
return m_preparingNewDocument;
}

bool DocumentManager::checkAndUpdateJson(
DocumentManager::Loadability DocumentManager::checkAndUpdateJson(
rapidjson::Value& obj, const score::GUIApplicationContext& ctx)
{
Loadability res;
if(obj.GetType() != rapidjson::kObjectType)
return false;
return res;

// Check the version
Version loaded_version{0};
Expand Down Expand Up @@ -712,7 +713,7 @@ bool DocumentManager::checkAndUpdateJson(
}
else
{
return false;
return res;
}
}

Expand All @@ -729,24 +730,26 @@ bool DocumentManager::checkAndUpdateJson(
else if(loaded_version < ctx.applicationSettings.saveFormatVersion)
{
// TODO update main
auto res
= updateJson(obj, loaded_version, ctx.applicationSettings.saveFormatVersion);
if(!res)
if(!updateJson(obj, loaded_version, ctx.applicationSettings.saveFormatVersion))
{
return false;
return res;
}
}

// Check the plug-ins
bool pluginsAvailable = true;
bool pluginsLoadable = true;

for(const auto& plug : loading_plugins)
{
auto it = local_plugins.find(plug.plugin);
if(it == local_plugins.end())
{
pluginsAvailable = false;
// Not fatal. Refusing here used to make a document unopenable on any
// machine that did not have every plug-in it mentions -- which is every
// machine, once builds differ by platform -- and it could not see a
// factory missing inside a plug-in that *is* present anyway, so it never
// gave the guarantee it appeared to.
res.missingPlugins.push_back(plug.plugin);
}
else
{
Expand All @@ -763,7 +766,10 @@ bool DocumentManager::checkAndUpdateJson(
}
}

return mainLoadable && pluginsAvailable && pluginsLoadable;
// A plug-in older than the file's is a different matter: its factory *is*
// found, and would read data in a format it does not understand.
res.loadable = mainLoadable && pluginsLoadable;
return res;
}

bool DocumentManager::updateJson(
Expand Down
17 changes: 15 additions & 2 deletions src/lib/core/presenter/DocumentManager.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
#include <score/plugins/qt_interfaces/PluginRequirements_QtInterface.hpp>
#include <score/tools/Version.hpp>

#include <core/document/DocumentBuilder.hpp>
Expand Down Expand Up @@ -107,11 +108,23 @@ class SCORE_LIB_BASE_EXPORT DocumentManager

bool preparingNewDocument() const;

struct Loadability
{
//! Whether the document can be opened at all.
bool loadable{};

//! Plug-ins the document names that this build does not have. It still
//! opens: what those plug-ins contributed -- processes, their ports,
//! devices -- is kept verbatim and written back unchanged, so the document
//! survives a round-trip through this machine.
std::vector<UuidKey<score::Plugin>> missingPlugins;
};

/**
* @brief checkAndUpdateJson
* @return boolean indicating if the document is loadable
* @return whether the document is loadable, and what it names that we lack
*/
static bool
static Loadability
checkAndUpdateJson(rapidjson::Value&, const score::GUIApplicationContext& ctx);

public:
Expand Down
37 changes: 37 additions & 0 deletions src/lib/score/application/ApplicationComponents.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ InterfaceListBase* ApplicationComponentsData::findInterfaceList(
return nullptr;
}

std::vector<std::pair<CommandGroupKey, CommandKey>>
ApplicationComponents::availableCommands() const
{
std::vector<std::pair<CommandGroupKey, CommandKey>> res;
res.reserve(m_data.commands.size());
for(const auto& [key, factory] : m_data.commands)
res.push_back(key);
return res;
}

Command* ApplicationComponents::instantiateUndoCommand(const CommandData& cmd) const
{
auto it = m_data.commands.find({cmd.parentKey, cmd.commandKey});
Expand All @@ -82,4 +92,31 @@ Command* ApplicationComponents::instantiateUndoCommand(const CommandData& cmd) c
#endif
return nullptr;
}

Command*
ApplicationComponents::instantiateUndoCommandIfAvailable(const CommandData& cmd) const noexcept
{
auto it = m_data.commands.find({cmd.parentKey, cmd.commandKey});
if(it == m_data.commands.end())
return nullptr;

try
{
return (*it->second)(cmd.data);
}
catch(const std::exception& e)
{
// The command exists but its payload does not deserialize here: it can name
// a factory this build lacks, e.g. a protocol compiled in under #if.
qDebug() << "Command" << cmd.parentKey.toString() << "::"
<< cmd.commandKey.toString() << "could not be read:" << e.what();
return nullptr;
}
catch(...)
{
qDebug() << "Command" << cmd.parentKey.toString()
<< "::" << cmd.commandKey.toString() << "could not be read.";
return nullptr;
}
}
}
14 changes: 14 additions & 0 deletions src/lib/score/application/ApplicationComponents.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,22 @@ class SCORE_LIB_BASE_EXPORT ApplicationComponents
throw;
}

//! Every command this build can instantiate, as {group, key}.
//!
//! Peers mirror each other by exchanging commands, so which ones exist is
//! part of what makes two builds able to work together.
std::vector<std::pair<CommandGroupKey, CommandKey>> availableCommands() const;

score::Command* instantiateUndoCommand(const CommandData& cmd) const;

//! Same, but returns nullptr instead of aborting or throwing when the command
//! is not registered in this build.
//!
//! For commands that did not originate locally: a peer in a networked session
//! may run a build with plug-ins we do not have, so an unknown command is a
//! situation to report, not a programming error to abort on.
score::Command* instantiateUndoCommandIfAvailable(const CommandData& cmd) const noexcept;

private:
const score::ApplicationComponentsData& m_data;
};
Expand Down
12 changes: 12 additions & 0 deletions src/lib/score/application/ApplicationContext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ struct SCORE_LIB_BASE_EXPORT ApplicationContext
return components.instantiateUndoCommand(cmd);
}

/**
* @brief Like instantiateUndoCommand, but returns nullptr for a command this
* build does not have, instead of aborting.
*
* For commands that did not originate locally: a peer in a networked session
* may run a build with plug-ins we do not have.
*/
auto instantiateUndoCommandIfAvailable(const CommandData& cmd) const noexcept
{
return components.instantiateUndoCommandIfAvailable(cmd);
}

const score::DocumentContext* currentDocument() const noexcept;

//! Access to start-up command-line settings
Expand Down
5 changes: 5 additions & 0 deletions src/lib/score/document/DocumentContext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class CommandStack;
class SelectionStack;
class ObjectLocker;
class DocumentPlugin;
class Environment;
struct SCORE_LIB_BASE_EXPORT DocumentContext
{
friend class score::Document;
Expand All @@ -31,6 +32,10 @@ struct SCORE_LIB_BASE_EXPORT DocumentContext

const std::vector<DocumentPlugin*>& pluginModels() const;

//! Where this document's files are: on this machine, or on the one running
//! the score. Ask rather than assuming a path can be opened.
score::Environment& environment() const noexcept;

template <typename T>
T& model() const
{
Expand Down
3 changes: 2 additions & 1 deletion src/lib/score/model/Component.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ SerializableComponentFactory::~SerializableComponentFactory() { }
SerializableComponentFactoryList::~SerializableComponentFactoryList() { }

score::SerializableComponent* SerializableComponentFactoryList::loadMissing(
const VisitorVariant& vis, const DocumentContext& ctx, QObject* parent) const
const UuidKey<score::SerializableComponent>& key, const VisitorVariant& vis,
const DocumentContext& ctx, QObject* parent) const
{
SCORE_TODO;
return nullptr;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/score/model/ComponentSerialization.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ struct SCORE_LIB_BASE_EXPORT SerializableComponentFactoryList
using object_type = score::SerializableComponent;
~SerializableComponentFactoryList();
score::SerializableComponent* loadMissing(
const VisitorVariant& vis, const score::DocumentContext& ctx,
QObject* parent) const;
const UuidKey<score::SerializableComponent>& key, const VisitorVariant& vis,
const score::DocumentContext& ctx, QObject* parent) const;
};

template <typename System_T>
Expand Down
Loading
Loading