cello
JUCE ValueTrees for Humans
Loading...
Searching...
No Matches
cello_ipc.cpp
1/*
2 Copyright (c) 2023 Brett g Porter
3 Permission is hereby granted, free of charge, to any person obtaining a copy
4 of this software and associated documentation files (the "Software"), to deal
5 in the Software without restriction, including without limitation the rights
6 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 copies of the Software, and to permit persons to whom the Software is
8 furnished to do so, subject to the following conditions:
9 The above copyright notice and this permission notice shall be included in all
10 copies or substantial portions of the Software.
11 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17 SOFTWARE.
18*/
19
20#include "cello_ipc.h"
21#include "JuceHeader.h"
22
23namespace
24{
25// each end of a client connection must use this number in their
26// headers. At some point it's probably worth finding a good way
27// to parameterize this.
28juce::uint32 CelloMagicIpcNumber { 0x000C3110 };
29} // namespace
30
31namespace juce
32{
33template <> struct VariantConverter<cello::IpcServerStatus>
34{
35 static cello::IpcServerStatus fromVar (const var& v) { return static_cast<cello::IpcServerStatus> (int (v)); }
36
37 static var toVar (const cello::IpcServerStatus& t) { return static_cast<int> (t); }
38};
39} // namespace juce
40
41namespace cello
42{
43
44IpcClient::IpcClient (Object& objectToWatch, UpdateType updateType, const juce::String& hostName, int portNum,
45 const juce::String& pipeName, int msTimeout, Object* state)
46: juce::InterprocessConnection { true, CelloMagicIpcNumber }
47, juce::ValueTreeSynchroniser { objectToWatch }
48, UpdateQueue { objectToWatch, nullptr }
49, clientProperties { objectToWatch.getType ().toString (), state }
50, update { updateType }
51, host { hostName }
52, port { portNum }
53, pipe { pipeName }
54, timeout { msTimeout }
55{
56 // verify that the update type makes basic sense
57 // need to either send or receive
58 jassert ((update & UpdateType::send) || (update & UpdateType::receive));
59 // ...and if we are sending a full update, we also need to be sending (in general)
60 jassert (!(update & UpdateType::fullUpdateOnConnect) ||
61 ((update & UpdateType::fullUpdateOnConnect) && (update & UpdateType::send)));
62}
63
64IpcClient::IpcClient (Object& objectToWatch, const juce::String& hostName, int portNum, int msTimeout,
65 UpdateType updateType, Object* state)
66: IpcClient (objectToWatch, updateType, hostName, portNum, "", msTimeout, state)
67{
68 jassert (host.isNotEmpty ());
69}
70
71IpcClient::IpcClient (Object& objectToWatch, const juce::String& pipeName, int msTimeout, UpdateType updateType,
72 Object* state)
73: IpcClient (objectToWatch, updateType, "", 0, pipeName, msTimeout, state)
74{
75 jassert (pipe.isNotEmpty ());
76}
77
78IpcClient::~IpcClient ()
79{
80 disconnect ();
81}
82
84{
85 if (host.isNotEmpty ())
86 {
87 // the options have no meaning for a socket connection, just try to
88 // connect to the specified socket.
89 return connectToSocket (host, port, timeout);
90 }
91
92 if (pipe.isNotEmpty ())
93 {
94 // else -- create and/or connect to a named pipe;
95 switch (options)
96 {
98 return createPipe (pipe, timeout, true);
100 return connectToPipe (pipe, timeout);
102 return createPipe (pipe, timeout, false);
104 default:
105 jassertfalse;
106 return false;
107 }
108 }
109 // invalid state -- no host *or* pipe defined.
110 jassertfalse;
111 return false;
112}
113
114void IpcClient::connectionMade ()
115{
116 clientProperties.connected = true;
117 if (update & UpdateType::fullUpdateOnConnect)
118 sendFullSyncCallback ();
119}
120
121void IpcClient::connectionLost ()
122{
123 clientProperties.connected = false;
124}
125
126void IpcClient::messageReceived (const juce::MemoryBlock& message)
127{
128 if (update & UpdateType::receive)
129 {
130 // create an rvalue copy of the message that can be moved from.
131 pushUpdate (juce::MemoryBlock { message });
132 clientProperties.rxCount++;
133 }
134}
135void IpcClient::stateChanged (const void* encodedChange, size_t encodedSize)
136{
137 if ((update & UpdateType::send) && (clientProperties.connected))
138 {
139 sendMessage ({ encodedChange, encodedSize });
140 clientProperties.txCount++;
141 }
142}
143
144//==============================================================================
145
146IpcServerProperties::IpcServerProperties (const juce::String& path, Object* state)
147: Object (path, state)
148{
149 // trigger callbacks when the status is set, whether the value changes or not.
150 status.forceUpdate (true);
151}
152
153void IpcServerProperties::startServer (int portNum, const juce::String& address)
154{
155 if (running)
156 {
157 status = IpcServerStatus::alreadyRunning;
158 return;
159 }
160 bindAddress = address;
161 // setting the port number triggers a callback in the server to actually
162 // start the server thread.
163 portNumber = portNum;
164}
165
167{
168 if (!running)
169 {
170 status = IpcServerStatus::alreadyStopped;
171 return;
172 }
173 bindAddress = "";
174 // setting the port # < 0 will trigger the server to stop.
175 portNumber = -1;
176}
177
178IpcServer::IpcServer (Object& sync, IpcClient::UpdateType updateType, const juce::String& statePath, Object* state)
179: syncObject { sync }
180, update { updateType }
181, serverProperties { statePath, state }
182{
183 // the server properties will change its portNumber member to let us
184 // know that we should start or stop ourselves.
185 serverProperties.portNumber.onPropertyChange (
186 [this] (juce::Identifier /*id*/)
187 {
188 if (serverProperties.portNumber > 0)
189 startServer (serverProperties.portNumber, serverProperties.bindAddress);
190 else
191 stopServer ();
192 });
193}
194
195IpcServer::~IpcServer ()
196{
197 // if the server is running, stop it.
198 stopServer ();
199 // ...and delete all of the connection objects.
200 connections.clear ();
201}
202
203bool IpcServer::startServer (int portNumber, const juce::String& bindAddress)
204{
205 if (serverProperties.running)
206 {
207 serverProperties.status = IpcServerStatus::alreadyRunning;
208 return true;
209 }
210
211 if (beginWaitingForSocket (portNumber, bindAddress))
212 {
213 serverProperties.running = true;
214 serverProperties.status = IpcServerStatus::startedOkay;
215 return true;
216 }
217
218 serverProperties.running = false;
219 serverProperties.status = IpcServerStatus::errorStarting;
220 return false;
221}
222
224{
225 if (!serverProperties.running)
226 {
227 serverProperties.status = IpcServerStatus::alreadyStopped;
228 return true;
229 }
230
231 stop ();
232 // the stop() method returns void, so we have no way to verify that
233 // the server actually did stop!
234
235 serverProperties.running = false;
236 serverProperties.status = IpcServerStatus::stoppedOkay;
237 return true;
238}
239
240juce::InterprocessConnection* IpcServer::createConnectionObject ()
241{
242 // create a new IpcConnection object, and take over its ownership;
243 // pass back a non-owning pointer to it so the base server class can
244 // finish setting up the client connection.
245 auto client { std::make_unique<IpcClient> (syncObject, "", 0, 0, update, &serverProperties) };
246 juce::InterprocessConnection* connection { client.get () };
247 connections.push_back (std::move (client));
248 return connection;
249}
250
251} // namespace cello
252
253#if RUN_UNIT_TESTS
254#include "test/test_cello_ipc.inl"
255#endif
IpcClient(Object &objectToWatch, const juce::String &hostName, int portNum, int msTimeout, UpdateType updateType, Object *state=nullptr)
Construct a new Ipc Client object that connects using sockets.
Definition cello_ipc.cpp:64
bool connect(ConnectOptions option=ConnectOptions::noOptions)
Attempt to make a connection to another IpcClient running in another process.
Definition cello_ipc.cpp:83
ConnectOptions
Definition cello_ipc.h:62
@ mustExist
pipe must already exist, fail if it doesn't
Definition cello_ipc.h:65
@ noOptions
default, used for sockets, which have no options.
Definition cello_ipc.h:63
@ createIfNeeded
If pipe exists, use it, otherwise create.
Definition cello_ipc.h:66
@ createOrFail
create the pipe, fail if we couldn't
Definition cello_ipc.h:64
bool startServer(int portNumber, const juce::String &bindAddress=juce::String())
Launch the thread that starts listening for incoming socket connections.
Definition cello_ipc.cpp:203
bool stopServer()
Stop the server.
Definition cello_ipc.cpp:223
juce::InterprocessConnection * createConnectionObject() override
When we get a connection, the base server class will call this so that we can create and return an in...
Definition cello_ipc.cpp:240
Definition cello_object.h:34
void onPropertyChange(juce::Identifier id, PropertyUpdateFn callback)
Install (or clear) a function to be called when one of this Object's properties changes....
Definition cello_object.cpp:255
void forceUpdate(bool shouldForceUpdate)
If passed true, any call that sets any Value property on this Object will result in a property change...
Definition cello_update_source.h:39
void stopServer()
Tell the server object we're controlling to stop.
Definition cello_ipc.cpp:166
void startServer(int portNum, const juce::String &address=juce::String())
Tell the server object we're controlling to start on the specified port (and optionally which address...
Definition cello_ipc.cpp:153