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 command_name;
99 AMCPCommand::ptr_type command;
100 error_state error = error_state::no_error;
101 std::shared_ptr<AMCPCommandQueue> queue;
104 //The paser method expects message to be complete messages with the delimiter stripped away.
105 //Thesefore the AMCPProtocolStrategy should be decorated with a delimiter_based_chunking_strategy
106 void Parse(const std::wstring& message, ClientInfoPtr client)
108 CASPAR_LOG(info) << L"Received message from " << client->address() << ": " << message << L"\\r\\n";
110 command_interpreter_result result;
111 if(interpret_command_string(message, result, client))
113 if(result.lock && !result.lock->check_access(client))
114 result.error = error_state::access_error;
116 result.queue->AddCommand(result.command);
119 if (result.error != error_state::no_error)
121 std::wstringstream answer;
125 case error_state::command_error:
126 answer << L"400 ERROR\r\n" << message << "\r\n";
128 case error_state::channel_error:
129 answer << L"401 " << result.command_name << " ERROR\r\n";
131 case error_state::parameters_error:
132 answer << L"402 " << result.command_name << " ERROR\r\n";
134 case error_state::access_error:
135 answer << L"503 " << result.command_name << " FAILED\r\n";
137 case error_state::unknown_error:
138 answer << L"500 FAILED\r\n";
141 CASPAR_THROW_EXCEPTION(programming_error()
142 << msg_info(L"Unhandled error_state enum constant " + boost::lexical_cast<std::wstring>(static_cast<int>(result.error))));
144 client->send(answer.str());
149 bool interpret_command_string(const std::wstring& message, command_interpreter_result& result, ClientInfoPtr client)
153 std::list<std::wstring> tokens;
154 tokenize(message, tokens);
157 if (!tokens.empty() && tokens.front().at(0) == L'/')
160 // Fail if no more tokens.
163 result.error = error_state::command_error;
167 // Consume command name
168 result.command_name = boost::to_upper_copy(tokens.front());
171 // Determine whether the next parameter is a channel spec or not
172 int channel_index = -1;
173 int layer_index = -1;
174 std::wstring channel_spec;
178 channel_spec = tokens.front();
179 std::wstring channelid_str = boost::trim_copy(channel_spec);
180 std::vector<std::wstring> split;
181 boost::split(split, channelid_str, boost::is_any_of("-"));
183 // Use non_throwing lexical cast to not hit exception break point all the time.
184 if (try_lexical_cast(split[0], channel_index))
188 if (split.size() > 1)
189 try_lexical_cast(split[1], layer_index);
191 // Consume channel-spec
196 bool is_channel_command = channel_index != -1;
198 // Create command instance
199 if (is_channel_command)
201 result.command = repo_->create_channel_command(result.command_name, client, channel_index, layer_index, tokens);
205 result.lock = repo_->channels().at(channel_index).lock;
206 result.queue = commandQueues_.at(channel_index + 1);
208 else // Might be a non channel command, although the first argument is numeric
210 // Restore backed up channel spec string.
211 tokens.push_front(channel_spec);
212 result.command = repo_->create_command(result.command_name, client, tokens);
215 result.queue = commandQueues_.at(0);
220 result.command = repo_->create_command(result.command_name, client, tokens);
223 result.queue = commandQueues_.at(0);
227 result.error = error_state::command_error;
230 std::vector<std::wstring> parameters(tokens.begin(), tokens.end());
232 result.command->parameters() = std::move(parameters);
234 if (result.command->parameters().size() < result.command->minimum_parameters())
235 result.error = error_state::parameters_error;
238 catch (std::out_of_range&)
240 CASPAR_LOG(error) << "Invalid channel specified.";
241 result.error = error_state::channel_error;
245 CASPAR_LOG_CURRENT_EXCEPTION();
246 result.error = error_state::unknown_error;
249 return result.error == error_state::no_error;
253 std::size_t tokenize(const std::wstring& message, C& pTokenVector)
255 //split on whitespace but keep strings within quotationmarks
256 //treat \ as the start of an escape-sequence: the following char will indicate what to actually put in the string
258 std::wstring currentToken;
260 bool inQuote = false;
261 bool getSpecialCode = false;
263 for(unsigned int charIndex=0; charIndex<message.size(); ++charIndex)
267 //insert code-handling here
268 switch(message[charIndex])
271 currentToken += L"\\";
274 currentToken += L"\"";
277 currentToken += L"\n";
282 getSpecialCode = false;
286 if(message[charIndex]==L'\\')
288 getSpecialCode = true;
292 if(message[charIndex]==L' ' && inQuote==false)
294 if(!currentToken.empty())
296 pTokenVector.push_back(currentToken);
297 currentToken.clear();
302 if(message[charIndex]==L'\"')
306 if(!currentToken.empty() || !inQuote)
308 pTokenVector.push_back(currentToken);
309 currentToken.clear();
314 currentToken += message[charIndex];
317 if(!currentToken.empty())
319 pTokenVector.push_back(currentToken);
320 currentToken.clear();
323 return pTokenVector.size();
327 AMCPProtocolStrategy::AMCPProtocolStrategy(const std::wstring& name, const spl::shared_ptr<amcp_command_repository>& repo)
328 : impl_(spl::make_unique<impl>(name, repo))
331 AMCPProtocolStrategy::~AMCPProtocolStrategy() {}
332 void AMCPProtocolStrategy::Parse(const std::wstring& msg, IO::ClientInfoPtr pClientInfo) { impl_->Parse(msg, pClientInfo); }
336 }} //namespace caspar