2 * Copyright (c) 2011 Sveriges Television AB <info@casparcg.com>
4 * This file is part of CasparCG (www.casparcg.com).
6 * CasparCG is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * CasparCG is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with CasparCG. If not, see <http://www.gnu.org/licenses/>.
19 * Author: Nicklas P Andersson
23 #include "../StdAfx.h"
25 #include "AMCPProtocolStrategy.h"
26 #include "amcp_shared.h"
27 #include "AMCPCommand.h"
28 #include "AMCPCommandQueue.h"
29 #include "amcp_command_repository.h"
37 #include <core/help/help_repository.h>
38 #include <core/help/help_sink.h>
40 #include <boost/algorithm/string/trim.hpp>
41 #include <boost/algorithm/string/split.hpp>
42 #include <boost/algorithm/string/replace.hpp>
43 #include <boost/lexical_cast.hpp>
46 #pragma warning (push, 1) // TODO: Legacy code, just disable warnings
49 namespace caspar { namespace protocol { namespace amcp {
51 using IO::ClientInfoPtr;
53 template <typename Out, typename In>
54 bool try_lexical_cast(const In& input, Out& result)
57 bool success = boost::conversion::detail::try_lexical_convert(input, result);
60 result = saved; // Needed because of how try_lexical_convert is implemented.
65 struct AMCPProtocolStrategy::impl
68 std::vector<AMCPCommandQueue::ptr_type> commandQueues_;
69 spl::shared_ptr<amcp_command_repository> repo_;
72 impl(const std::wstring& name, const spl::shared_ptr<amcp_command_repository>& repo)
75 commandQueues_.push_back(spl::make_shared<AMCPCommandQueue>(L"General Queue for " + name));
77 for (int i = 0; i < repo_->channels().size(); ++i)
79 commandQueues_.push_back(spl::make_shared<AMCPCommandQueue>(
80 L"Channel " + boost::lexical_cast<std::wstring>(i + 1) + L" for " + name));
86 enum class error_state {
95 struct command_interpreter_result
97 std::shared_ptr<caspar::IO::lock_container> lock;
98 std::wstring request_id;
99 std::wstring command_name;
100 AMCPCommand::ptr_type command;
101 error_state error = error_state::no_error;
102 std::shared_ptr<AMCPCommandQueue> queue;
105 //The paser method expects message to be complete messages with the delimiter stripped away.
106 //Thesefore the AMCPProtocolStrategy should be decorated with a delimiter_based_chunking_strategy
107 void Parse(const std::wstring& message, ClientInfoPtr client)
109 CASPAR_LOG_COMMUNICATION(info) << L"Received message from " << client->address() << ": " << message << L"\\r\\n";
111 command_interpreter_result result;
112 if(interpret_command_string(message, result, client))
114 if(result.lock && !result.lock->check_access(client))
115 result.error = error_state::access_error;
117 result.queue->AddCommand(result.command);
120 if (result.error != error_state::no_error)
122 std::wstringstream answer;
124 if (!result.request_id.empty())
125 answer << L"RES " << result.request_id << L" ";
129 case error_state::command_error:
130 answer << L"400 ERROR\r\n" << message << "\r\n";
132 case error_state::channel_error:
133 answer << L"401 " << result.command_name << " ERROR\r\n";
135 case error_state::parameters_error:
136 answer << L"402 " << result.command_name << " ERROR\r\n";
138 case error_state::access_error:
139 answer << L"503 " << result.command_name << " FAILED\r\n";
141 case error_state::unknown_error:
142 answer << L"500 FAILED\r\n";
145 CASPAR_THROW_EXCEPTION(programming_error()
146 << msg_info(L"Unhandled error_state enum constant " + boost::lexical_cast<std::wstring>(static_cast<int>(result.error))));
148 client->send(answer.str());
153 bool interpret_command_string(const std::wstring& message, command_interpreter_result& result, ClientInfoPtr client)
157 std::list<std::wstring> tokens;
158 tokenize(message, tokens);
161 if (!tokens.empty() && tokens.front().at(0) == L'/')
164 if (!tokens.empty() && boost::iequals(tokens.front(), L"REQ"))
170 result.error = error_state::parameters_error;
174 result.request_id = tokens.front();
178 // Fail if no more tokens.
181 result.error = error_state::command_error;
185 // Consume command name
186 result.command_name = boost::to_upper_copy(tokens.front());
189 // Determine whether the next parameter is a channel spec or not
190 int channel_index = -1;
191 int layer_index = -1;
192 std::wstring channel_spec;
196 channel_spec = tokens.front();
197 std::wstring channelid_str = boost::trim_copy(channel_spec);
198 std::vector<std::wstring> split;
199 boost::split(split, channelid_str, boost::is_any_of("-"));
201 // Use non_throwing lexical cast to not hit exception break point all the time.
202 if (try_lexical_cast(split[0], channel_index))
206 if (split.size() > 1)
207 try_lexical_cast(split[1], layer_index);
209 // Consume channel-spec
214 bool is_channel_command = channel_index != -1;
216 // Create command instance
217 if (is_channel_command)
219 result.command = repo_->create_channel_command(result.command_name, client, channel_index, layer_index, tokens);
223 result.lock = repo_->channels().at(channel_index).lock;
224 result.queue = commandQueues_.at(channel_index + 1);
226 else // Might be a non channel command, although the first argument is numeric
228 // Restore backed up channel spec string.
229 tokens.push_front(channel_spec);
230 result.command = repo_->create_command(result.command_name, client, tokens);
233 result.queue = commandQueues_.at(0);
238 result.command = repo_->create_command(result.command_name, client, tokens);
241 result.queue = commandQueues_.at(0);
245 result.error = error_state::command_error;
248 std::vector<std::wstring> parameters(tokens.begin(), tokens.end());
250 result.command->parameters() = std::move(parameters);
252 if (result.command->parameters().size() < result.command->minimum_parameters())
253 result.error = error_state::parameters_error;
257 result.command->set_request_id(result.request_id);
259 catch (std::out_of_range&)
261 CASPAR_LOG(error) << "Invalid channel specified.";
262 result.error = error_state::channel_error;
266 CASPAR_LOG_CURRENT_EXCEPTION();
267 result.error = error_state::unknown_error;
270 return result.error == error_state::no_error;
274 std::size_t tokenize(const std::wstring& message, C& pTokenVector)
276 //split on whitespace but keep strings within quotationmarks
277 //treat \ as the start of an escape-sequence: the following char will indicate what to actually put in the string
279 std::wstring currentToken;
281 bool inQuote = false;
282 bool getSpecialCode = false;
284 for(unsigned int charIndex=0; charIndex<message.size(); ++charIndex)
288 //insert code-handling here
289 switch(message[charIndex])
292 currentToken += L"\\";
295 currentToken += L"\"";
298 currentToken += L"\n";
303 getSpecialCode = false;
307 if(message[charIndex]==L'\\')
309 getSpecialCode = true;
313 if(message[charIndex]==L' ' && inQuote==false)
315 if(!currentToken.empty())
317 pTokenVector.push_back(currentToken);
318 currentToken.clear();
323 if(message[charIndex]==L'\"')
327 if(!currentToken.empty() || !inQuote)
329 pTokenVector.push_back(currentToken);
330 currentToken.clear();
335 currentToken += message[charIndex];
338 if(!currentToken.empty())
340 pTokenVector.push_back(currentToken);
341 currentToken.clear();
344 return pTokenVector.size();
348 AMCPProtocolStrategy::AMCPProtocolStrategy(const std::wstring& name, const spl::shared_ptr<amcp_command_repository>& repo)
349 : impl_(spl::make_unique<impl>(name, repo))
352 AMCPProtocolStrategy::~AMCPProtocolStrategy() {}
353 void AMCPProtocolStrategy::Parse(const std::wstring& msg, IO::ClientInfoPtr pClientInfo) { impl_->Parse(msg, pClientInfo); }
357 }} //namespace caspar