From 4f2610dece8b1b5f1d24b4d046a2bfbe2f2e2cc6 Mon Sep 17 00:00:00 2001
From: outfoxxed <outfoxxed@outfoxxed.me>
Date: Sun, 26 Jan 2025 03:57:07 -0800
Subject: [PATCH] io/ipchandler: add `prop get`

---
 src/io/ipc.cpp              |  27 +++++++++
 src/io/ipc.hpp              |  26 ++++++++-
 src/io/ipccomm.cpp          | 107 +++++++++++++++++++++++++++++++-----
 src/io/ipccomm.hpp          |  18 +++++-
 src/io/ipchandler.cpp       |  52 ++++++++++++++++++
 src/io/ipchandler.hpp       |  45 +++++++++++----
 src/ipc/ipccommand.hpp      |   3 +-
 src/launch/command.cpp      |   2 +
 src/launch/launch_p.hpp     |   1 +
 src/launch/parsecommand.cpp |  12 ++++
 10 files changed, 262 insertions(+), 31 deletions(-)

diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp
index 37a37eb3..768299ed 100644
--- a/src/io/ipc.cpp
+++ b/src/io/ipc.cpp
@@ -1,9 +1,12 @@
 #include "ipc.hpp"
+#include <cstring>
 #include <utility>
 
 #include <qcolor.h>
 #include <qmetatype.h>
 #include <qobjectdefs.h>
+#include <qtypes.h>
+#include <qvariant.h>
 
 namespace qs::io::ipc {
 
@@ -14,6 +17,12 @@ const BoolIpcType BoolIpcType::INSTANCE {};
 const DoubleIpcType DoubleIpcType::INSTANCE {};
 const ColorIpcType ColorIpcType::INSTANCE {};
 
+void* IpcType::copyStorage(const void* data) const {
+	auto* storage = this->createStorage();
+	memcpy(storage, data, this->size());
+	return storage;
+}
+
 const IpcType* IpcType::ipcType(const QMetaType& metaType) {
 	if (metaType.id() == QMetaType::Void) return &VoidIpcType::INSTANCE;
 	if (metaType.id() == QMetaType::QString) return &StringIpcType::INSTANCE;
@@ -70,12 +79,18 @@ void IpcTypeSlot::replace(void* value) {
 	this->storage = value;
 }
 
+void IpcTypeSlot::replace(const QVariant& value) {
+	this->replace(this->mType->copyStorage(value.constData()));
+}
+
 const char* VoidIpcType::name() const { return "void"; }
 const char* VoidIpcType::genericArgumentName() const { return "void"; }
+qsizetype VoidIpcType::size() const { return 0; }
 
 // string
 const char* StringIpcType::name() const { return "string"; }
 const char* StringIpcType::genericArgumentName() const { return "QString"; }
+qsizetype StringIpcType::size() const { return sizeof(QString); }
 void* StringIpcType::fromString(const QString& string) const { return new QString(string); }
 QString StringIpcType::toString(void* slot) const { return *static_cast<QString*>(slot); }
 void* StringIpcType::createStorage() const { return new QString(); }
@@ -84,6 +99,7 @@ void StringIpcType::destroyStorage(void* slot) const { delete static_cast<QStrin
 // int
 const char* IntIpcType::name() const { return "int"; }
 const char* IntIpcType::genericArgumentName() const { return "int"; }
+qsizetype IntIpcType::size() const { return sizeof(int); }
 
 void* IntIpcType::fromString(const QString& string) const {
 	auto ok = false;
@@ -100,6 +116,7 @@ void IntIpcType::destroyStorage(void* slot) const { delete static_cast<int*>(slo
 // bool
 const char* BoolIpcType::name() const { return "bool"; }
 const char* BoolIpcType::genericArgumentName() const { return "bool"; }
+qsizetype BoolIpcType::size() const { return sizeof(bool); }
 
 void* BoolIpcType::fromString(const QString& string) const {
 	if (string == "true") return new bool(true);
@@ -121,6 +138,7 @@ void BoolIpcType::destroyStorage(void* slot) const { delete static_cast<bool*>(s
 // double
 const char* DoubleIpcType::name() const { return "real"; }
 const char* DoubleIpcType::genericArgumentName() const { return "double"; }
+qsizetype DoubleIpcType::size() const { return sizeof(double); }
 
 void* DoubleIpcType::fromString(const QString& string) const {
 	auto ok = false;
@@ -139,6 +157,7 @@ void DoubleIpcType::destroyStorage(void* slot) const { delete static_cast<double
 // color
 const char* ColorIpcType::name() const { return "color"; }
 const char* ColorIpcType::genericArgumentName() const { return "QColor"; }
+qsizetype ColorIpcType::size() const { return sizeof(QColor); }
 
 void* ColorIpcType::fromString(const QString& string) const {
 	auto color = QColor::fromString(string);
@@ -167,6 +186,10 @@ QString WireFunctionDefinition::toString() const {
 	return "function " % this->name % '(' % paramString % "): " % this->returnType;
 }
 
+QString WirePropertyDefinition::toString() const {
+	return "property " % this->name % ": " % this->type;
+}
+
 QString WireTargetDefinition::toString() const {
 	QString accum = "target " % this->name;
 
@@ -174,6 +197,10 @@ QString WireTargetDefinition::toString() const {
 		accum += "\n  " % func.toString();
 	}
 
+	for (const auto& prop: this->properties) {
+		accum += "\n  " % prop.toString();
+	}
+
 	return accum;
 }
 
diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp
index 924f045e..d2b865a2 100644
--- a/src/io/ipc.hpp
+++ b/src/io/ipc.hpp
@@ -3,6 +3,7 @@
 #include <qcontainerfwd.h>
 #include <qobjectdefs.h>
 #include <qtclasshelpermacros.h>
+#include <qtypes.h>
 
 #include "../ipc/ipc.hpp"
 
@@ -21,10 +22,12 @@ public:
 
 	[[nodiscard]] virtual const char* name() const = 0;
 	[[nodiscard]] virtual const char* genericArgumentName() const = 0;
+	[[nodiscard]] virtual qsizetype size() const = 0;
 	[[nodiscard]] virtual void* fromString(const QString& /*string*/) const { return nullptr; }
 	[[nodiscard]] virtual QString toString(void* /*slot*/) const { return ""; }
 	[[nodiscard]] virtual void* createStorage() const { return nullptr; }
 	virtual void destroyStorage(void* /*slot*/) const {}
+	void* copyStorage(const void* data) const;
 
 	static const IpcType* ipcType(const QMetaType& metaType);
 };
@@ -43,6 +46,7 @@ public:
 	[[nodiscard]] QGenericReturnArgument asGenericReturnArgument();
 
 	void replace(void* value);
+	void replace(const QVariant& value);
 
 private:
 	const IpcType* mType = nullptr;
@@ -53,6 +57,7 @@ class VoidIpcType: public IpcType {
 public:
 	[[nodiscard]] const char* name() const override;
 	[[nodiscard]] const char* genericArgumentName() const override;
+	[[nodiscard]] qsizetype size() const override;
 
 	static const VoidIpcType INSTANCE;
 };
@@ -61,6 +66,7 @@ class StringIpcType: public IpcType {
 public:
 	[[nodiscard]] const char* name() const override;
 	[[nodiscard]] const char* genericArgumentName() const override;
+	[[nodiscard]] qsizetype size() const override;
 	[[nodiscard]] void* fromString(const QString& string) const override;
 	[[nodiscard]] QString toString(void* slot) const override;
 	[[nodiscard]] void* createStorage() const override;
@@ -73,6 +79,7 @@ class IntIpcType: public IpcType {
 public:
 	[[nodiscard]] const char* name() const override;
 	[[nodiscard]] const char* genericArgumentName() const override;
+	[[nodiscard]] qsizetype size() const override;
 	[[nodiscard]] void* fromString(const QString& string) const override;
 	[[nodiscard]] QString toString(void* slot) const override;
 	[[nodiscard]] void* createStorage() const override;
@@ -85,6 +92,7 @@ class BoolIpcType: public IpcType {
 public:
 	[[nodiscard]] const char* name() const override;
 	[[nodiscard]] const char* genericArgumentName() const override;
+	[[nodiscard]] qsizetype size() const override;
 	[[nodiscard]] void* fromString(const QString& string) const override;
 	[[nodiscard]] QString toString(void* slot) const override;
 	[[nodiscard]] void* createStorage() const override;
@@ -97,6 +105,7 @@ class DoubleIpcType: public IpcType {
 public:
 	[[nodiscard]] const char* name() const override;
 	[[nodiscard]] const char* genericArgumentName() const override;
+	[[nodiscard]] qsizetype size() const override;
 	[[nodiscard]] void* fromString(const QString& string) const override;
 	[[nodiscard]] QString toString(void* slot) const override;
 	[[nodiscard]] void* createStorage() const override;
@@ -109,6 +118,7 @@ class ColorIpcType: public IpcType {
 public:
 	[[nodiscard]] const char* name() const override;
 	[[nodiscard]] const char* genericArgumentName() const override;
+	[[nodiscard]] qsizetype size() const override;
 	[[nodiscard]] void* fromString(const QString& string) const override;
 	[[nodiscard]] QString toString(void* slot) const override;
 	[[nodiscard]] void* createStorage() const override;
@@ -127,13 +137,23 @@ struct WireFunctionDefinition {
 
 DEFINE_SIMPLE_DATASTREAM_OPS(WireFunctionDefinition, data.name, data.returnType, data.arguments);
 
-struct WireTargetDefinition {
+struct WirePropertyDefinition {
 	QString name;
-	QVector<WireFunctionDefinition> functions;
+	QString type;
 
 	[[nodiscard]] QString toString() const;
 };
 
-DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions);
+DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type);
+
+struct WireTargetDefinition {
+	QString name;
+	QVector<WireFunctionDefinition> functions;
+	QVector<WirePropertyDefinition> properties;
+
+	[[nodiscard]] QString toString() const;
+};
+
+DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties);
 
 } // namespace qs::io::ipc
diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp
index f2a9118a..7203a307 100644
--- a/src/io/ipccomm.cpp
+++ b/src/io/ipccomm.cpp
@@ -21,16 +21,17 @@ namespace qs::io::ipc::comm {
 
 struct NoCurrentGeneration: std::monostate {};
 struct TargetNotFound: std::monostate {};
-struct FunctionNotFound: std::monostate {};
+struct EntryNotFound: std::monostate {};
 
 using QueryResponse = std::variant<
     std::monostate,
     NoCurrentGeneration,
     TargetNotFound,
-    FunctionNotFound,
+    EntryNotFound,
     QVector<WireTargetDefinition>,
     WireTargetDefinition,
-    WireFunctionDefinition>;
+    WireFunctionDefinition,
+    WirePropertyDefinition>;
 
 void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
 	auto resp = conn->responseStream<QueryResponse>();
@@ -44,16 +45,24 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
 			auto* handler = registry->findHandler(this->target);
 
 			if (handler) {
-				if (this->function.isEmpty()) {
+				if (this->name.isEmpty()) {
 					resp << handler->wireDef();
 				} else {
-					auto* func = handler->findFunction(this->function);
+					auto* func = handler->findFunction(this->name);
 
 					if (func) {
 						resp << func->wireDef();
-					} else {
-						resp << FunctionNotFound();
+						return;
 					}
+
+					auto* prop = handler->findProperty(this->name);
+
+					if (prop) {
+						resp << prop->wireDef();
+						return;
+					}
+
+					resp << EntryNotFound();
 				}
 			} else {
 				resp << TargetNotFound();
@@ -64,8 +73,8 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
 	}
 }
 
-int queryMetadata(IpcClient* client, const QString& target, const QString& function) {
-	client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .function = function}));
+int queryMetadata(IpcClient* client, const QString& target, const QString& name) {
+	client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .name = name}));
 
 	QueryResponse slot;
 	if (!client->waitForResponse(slot)) return -1;
@@ -82,9 +91,11 @@ int queryMetadata(IpcClient* client, const QString& target, const QString& funct
 		qCInfo(logBare).noquote() << std::get<WireTargetDefinition>(slot).toString();
 	} else if (std::holds_alternative<WireFunctionDefinition>(slot)) {
 		qCInfo(logBare).noquote() << std::get<WireFunctionDefinition>(slot).toString();
+	} else if (std::holds_alternative<WirePropertyDefinition>(slot)) {
+		qCInfo(logBare).noquote() << std::get<WirePropertyDefinition>(slot).toString();
 	} else if (std::holds_alternative<TargetNotFound>(slot)) {
 		qCCritical(logBare) << "Target not found.";
-	} else if (std::holds_alternative<FunctionNotFound>(slot)) {
+	} else if (std::holds_alternative<EntryNotFound>(slot)) {
 		qCCritical(logBare) << "Function not found.";
 	} else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
 		qCCritical(logBare) << "Not ready to accept queries yet.";
@@ -119,7 +130,7 @@ using StringCallResponse = std::variant<
     std::monostate,
     NoCurrentGeneration,
     TargetNotFound,
-    FunctionNotFound,
+    EntryNotFound,
     ArgParseFailed,
     Completed>;
 
@@ -137,7 +148,7 @@ void StringCallCommand::exec(qs::ipc::IpcServerConnection* conn) const {
 
 		auto* func = handler->findFunction(this->function);
 		if (!func) {
-			resp << FunctionNotFound();
+			resp << EntryNotFound();
 			return;
 		}
 
@@ -223,7 +234,7 @@ int callFunction(
 		qCCritical(logBare).noquote() << "Function definition:" << error.definition.toString();
 	} else if (std::holds_alternative<TargetNotFound>(slot)) {
 		qCCritical(logBare) << "Target not found.";
-	} else if (std::holds_alternative<FunctionNotFound>(slot)) {
+	} else if (std::holds_alternative<EntryNotFound>(slot)) {
 		qCCritical(logBare) << "Function not found.";
 	} else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
 		qCCritical(logBare) << "Not ready to accept queries yet.";
@@ -233,4 +244,74 @@ int callFunction(
 
 	return -1;
 }
+
+struct PropertyValue {
+	QString value;
+};
+
+DEFINE_SIMPLE_DATASTREAM_OPS(PropertyValue, data.value);
+
+using StringPropReadResponse =
+    std::variant<std::monostate, NoCurrentGeneration, TargetNotFound, EntryNotFound, PropertyValue>;
+
+void StringPropReadCommand::exec(qs::ipc::IpcServerConnection* conn) const {
+	auto resp = conn->responseStream<StringPropReadResponse>();
+
+	if (auto* generation = EngineGeneration::currentGeneration()) {
+		auto* registry = IpcHandlerRegistry::forGeneration(generation);
+
+		auto* handler = registry->findHandler(this->target);
+		if (!handler) {
+			resp << TargetNotFound();
+			return;
+		}
+
+		auto* prop = handler->findProperty(this->property);
+		if (!prop) {
+			resp << EntryNotFound();
+			return;
+		}
+
+		auto slot = IpcTypeSlot(prop->type);
+		prop->read(handler, slot);
+
+		resp << PropertyValue {
+		    .value = slot.type()->toString(slot.get()),
+		};
+	} else {
+		conn->respond(StringCallResponse(NoCurrentGeneration()));
+	}
+}
+
+int getProperty(IpcClient* client, const QString& target, const QString& property) {
+	if (target.isEmpty()) {
+		qCCritical(logBare) << "Target required to send message.";
+		return -1;
+	} else if (property.isEmpty()) {
+		qCCritical(logBare) << "Property required to send message.";
+		return -1;
+	}
+
+	client->sendMessage(IpcCommand(StringPropReadCommand {.target = target, .property = property}));
+
+	StringPropReadResponse slot;
+	if (!client->waitForResponse(slot)) return -1;
+
+	if (std::holds_alternative<PropertyValue>(slot)) {
+		auto& result = std::get<PropertyValue>(slot);
+		QTextStream(stdout) << result.value << Qt::endl;
+		return 0;
+	} else if (std::holds_alternative<TargetNotFound>(slot)) {
+		qCCritical(logBare) << "Target not found.";
+	} else if (std::holds_alternative<EntryNotFound>(slot)) {
+		qCCritical(logBare) << "Property not found.";
+	} else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
+		qCCritical(logBare) << "Not ready to accept queries yet.";
+	} else {
+		qCCritical(logIpc) << "Received invalid IPC response from" << client;
+	}
+
+	return -1;
+}
+
 } // namespace qs::io::ipc::comm
diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp
index 69463983..bc7dbf97 100644
--- a/src/io/ipccomm.hpp
+++ b/src/io/ipccomm.hpp
@@ -2,6 +2,7 @@
 
 #include <qcontainerfwd.h>
 #include <qflags.h>
+#include <qtypes.h>
 
 #include "../ipc/ipc.hpp"
 
@@ -9,12 +10,12 @@ namespace qs::io::ipc::comm {
 
 struct QueryMetadataCommand {
 	QString target;
-	QString function;
+	QString name;
 
 	void exec(qs::ipc::IpcServerConnection* conn) const;
 };
 
-DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.function);
+DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.name);
 
 struct StringCallCommand {
 	QString target;
@@ -27,7 +28,7 @@ struct StringCallCommand {
 DEFINE_SIMPLE_DATASTREAM_OPS(StringCallCommand, data.target, data.function, data.arguments);
 
 void handleMsg(qs::ipc::IpcServerConnection* conn);
-int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& function);
+int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& name);
 
 int callFunction(
     qs::ipc::IpcClient* client,
@@ -36,4 +37,15 @@ int callFunction(
     const QVector<QString>& arguments
 );
 
+struct StringPropReadCommand {
+	QString target;
+	QString property;
+
+	void exec(qs::ipc::IpcServerConnection* conn) const;
+};
+
+DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property);
+
+int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property);
+
 } // namespace qs::io::ipc::comm
diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp
index 510b2055..517e4505 100644
--- a/src/io/ipchandler.cpp
+++ b/src/io/ipchandler.cpp
@@ -107,6 +107,32 @@ WireFunctionDefinition IpcFunction::wireDef() const {
 	return wire;
 }
 
+bool IpcProperty::resolve(QString& error) {
+	this->type = IpcType::ipcType(this->property.metaType());
+
+	if (!this->type) {
+		error = QString("Type %1 cannot be used across IPC.").arg(this->property.metaType().name());
+		return false;
+	}
+
+	return true;
+}
+
+void IpcProperty::read(QObject* target, IpcTypeSlot& slot) const {
+	slot.replace(this->property.read(target));
+}
+
+QString IpcProperty::toString() const {
+	return QString("property ") % this->property.name() % ": " % this->type->name();
+}
+
+WirePropertyDefinition IpcProperty::wireDef() const {
+	WirePropertyDefinition wire;
+	wire.name = this->property.name();
+	wire.type = this->type->name();
+	return wire;
+}
+
 IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) {
 	for (const auto& arg: function.argumentTypes) {
 		this->argumentSlots.emplace_back(arg);
@@ -153,6 +179,21 @@ void IpcHandler::onPostReload() {
 		}
 	}
 
+	for (auto i = smeta.propertyCount(); i != meta->propertyCount(); i++) {
+		const auto& property = meta->property(i);
+		if (!property.isReadable() || !property.hasNotifySignal()) continue;
+
+		auto ipcProp = IpcProperty(property);
+		QString error;
+
+		if (!ipcProp.resolve(error)) {
+			qmlWarning(this).nospace().noquote()
+			    << "Error parsing property \"" << property.name() << "\": " << error;
+		} else {
+			this->propertyMap.insert(property.name(), ipcProp);
+		}
+	}
+
 	this->complete = true;
 	this->updateRegistration();
 
@@ -270,6 +311,10 @@ WireTargetDefinition IpcHandler::wireDef() const {
 		wire.functions += func.wireDef();
 	}
 
+	for (const auto& prop: this->propertyMap.values()) {
+		wire.properties += prop.wireDef();
+	}
+
 	return wire;
 }
 
@@ -307,6 +352,13 @@ IpcFunction* IpcHandler::findFunction(const QString& name) {
 	else return &*itr;
 }
 
+IpcProperty* IpcHandler::findProperty(const QString& name) {
+	auto itr = this->propertyMap.find(name);
+
+	if (itr == this->propertyMap.end()) return nullptr;
+	else return &*itr;
+}
+
 IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) {
 	return this->handlers.value(target);
 }
diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp
index cc4ee5f4..e6b24ba1 100644
--- a/src/io/ipchandler.hpp
+++ b/src/io/ipchandler.hpp
@@ -53,14 +53,28 @@ private:
 	friend class IpcFunction;
 };
 
+class IpcProperty {
+public:
+	explicit IpcProperty(QMetaProperty property): property(property) {}
+
+	bool resolve(QString& error);
+	void read(QObject* target, IpcTypeSlot& slot) const;
+
+	[[nodiscard]] QString toString() const;
+	[[nodiscard]] WirePropertyDefinition wireDef() const;
+
+	QMetaProperty property;
+	const IpcType* type = nullptr;
+};
+
 class IpcHandlerRegistry;
 
 ///! Handler for IPC message calls.
 /// Each IpcHandler is registered into a per-instance map by its unique @@target.
-/// Functions defined on the IpcHandler can be called by `qs msg`.
+/// Functions and properties defined on the IpcHandler can be accessed via `qs ipc`.
 ///
 /// #### Handler Functions
-/// IPC handler functions can be called by `qs msg` as long as they have at most 10
+/// IPC handler functions can be called by `qs ipc call` as long as they have at most 10
 /// arguments, and all argument types along with the return type are listed below.
 ///
 /// **Argument and return types must be explicitly specified or they will not
@@ -112,9 +126,9 @@ class IpcHandlerRegistry;
 ///   }
 /// }
 /// ```
-/// The list of registered targets can be inspected using `qs msg -s`.
+/// The list of registered targets can be inspected using `qs ipc show`.
 /// ```sh
-/// $ qs msg -s
+/// $ qs ipc show
 /// target rect
 ///   function setColor(color: color): void
 ///   function getColor(): color
@@ -124,18 +138,22 @@ class IpcHandlerRegistry;
 ///   function getRadius(): int
 /// ```
 ///
-/// and then invoked using `qs msg`.
+/// and then invoked using `qs ipc call`.
 /// ```sh
-/// $ qs msg rect setColor orange
-/// $ qs msg rect setAngle 40.5
-/// $ qs msg rect setRadius 30
-/// $ qs msg rect getColor
+/// $ qs ipc call rect setColor orange
+/// $ qs ipc call rect setAngle 40.5
+/// $ qs ipc call rect setRadius 30
+/// $ qs ipc call rect getColor
 /// #ffffa500
-/// $ qs msg rect getAngle
+/// $ qs ipc call rect getAngle
 /// 40.5
-/// $ qs msg rect getRadius
+/// $ qs ipc call rect getRadius
 /// 30
 /// ```
+///
+/// #### Properties
+/// Properties of an IpcHanlder can be read using `qs ipc prop get` as long as they are
+/// of an IPC compatible type. See the table above for compatible types.
 class IpcHandler
     : public QObject
     , public PostReloadHook {
@@ -162,12 +180,16 @@ public:
 
 	QString listMembers(qsizetype indent);
 	[[nodiscard]] IpcFunction* findFunction(const QString& name);
+	[[nodiscard]] IpcProperty* findProperty(const QString& name);
 	[[nodiscard]] WireTargetDefinition wireDef() const;
 
 signals:
 	void enabledChanged();
 	void targetChanged();
 
+private slots:
+	//void handleIpcPropertyChange();
+
 private:
 	void updateRegistration(bool destroying = false);
 
@@ -183,6 +205,7 @@ private:
 	bool complete = false;
 
 	QHash<QString, IpcFunction> functionMap;
+	QHash<QString, IpcProperty> propertyMap;
 
 	friend class IpcHandlerRegistry;
 };
diff --git a/src/ipc/ipccommand.hpp b/src/ipc/ipccommand.hpp
index c2e5059f..b221b460 100644
--- a/src/ipc/ipccommand.hpp
+++ b/src/ipc/ipccommand.hpp
@@ -15,6 +15,7 @@ using IpcCommand = std::variant<
     std::monostate,
     IpcKillCommand,
     qs::io::ipc::comm::QueryMetadataCommand,
-    qs::io::ipc::comm::StringCallCommand>;
+    qs::io::ipc::comm::StringCallCommand,
+    qs::io::ipc::comm::StringPropReadCommand>;
 
 } // namespace qs::ipc
diff --git a/src/launch/command.cpp b/src/launch/command.cpp
index f814b5ff..00ad613e 100644
--- a/src/launch/command.cpp
+++ b/src/launch/command.cpp
@@ -293,6 +293,8 @@ int ipcCommand(CommandState& cmd) {
 	return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
 		if (*cmd.ipc.show || cmd.ipc.showOld) {
 			return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name);
+		} else if (*cmd.ipc.getprop) {
+			return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name);
 		} else {
 			QVector<QString> arguments;
 			for (auto& arg: cmd.ipc.arguments) {
diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp
index a9a515c4..77808450 100644
--- a/src/launch/launch_p.hpp
+++ b/src/launch/launch_p.hpp
@@ -71,6 +71,7 @@ struct CommandState {
 		CLI::App* ipc = nullptr;
 		CLI::App* show = nullptr;
 		CLI::App* call = nullptr;
+		CLI::App* getprop = nullptr;
 		bool showOld = false;
 		QStringOption target;
 		QStringOption name;
diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp
index 2c082fec..1edbf01e 100644
--- a/src/launch/parsecommand.cpp
+++ b/src/launch/parsecommand.cpp
@@ -194,6 +194,18 @@ int parseCommand(int argc, char** argv, CommandState& state) {
 			    ->description("Arguments to the called function.")
 			    ->allow_extra_args();
 		}
+
+		{
+			auto* prop =
+			    sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand();
+
+			{
+				auto* get = prop->add_subcommand("get", "Read the value of a property.");
+				state.ipc.getprop = get;
+				get->add_option("target", state.ipc.target, "The target to read the property of.");
+				get->add_option("property", state.ipc.name)->description("The property to read.");
+			}
+		}
 	}
 
 	{