face-module.cpp
Go to the documentation of this file.
1 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
2 /*
3  * Copyright (c) 2014-2023, Regents of the University of California,
4  * Arizona Board of Regents,
5  * Colorado State University,
6  * University Pierre & Marie Curie, Sorbonne University,
7  * Washington University in St. Louis,
8  * Beijing Institute of Technology,
9  * The University of Memphis.
10  *
11  * This file is part of NFD (Named Data Networking Forwarding Daemon).
12  * See AUTHORS.md for complete list of NFD authors and contributors.
13  *
14  * NFD is free software: you can redistribute it and/or modify it under the terms
15  * of the GNU General Public License as published by the Free Software Foundation,
16  * either version 3 of the License, or (at your option) any later version.
17  *
18  * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
19  * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
20  * PURPOSE. See the GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License along with
23  * NFD, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
24  */
25 
26 #include "face-module.hpp"
27 #include "face-helpers.hpp"
28 
29 #include <ndn-cxx/mgmt/nfd/status-dataset.hpp>
30 
31 #include <boost/lexical_cast/try_lexical_convert.hpp>
32 
33 namespace nfd::tools::nfdc {
34 
35 void
37 {
38  CommandDefinition defFaceList("face", "list");
39  defFaceList
40  .setTitle("print face list")
44  parser.addCommand(defFaceList, &FaceModule::list);
45  parser.addAlias("face", "list", "");
46 
47  CommandDefinition defFaceShow("face", "show");
48  defFaceShow
49  .setTitle("show face information")
51  parser.addCommand(defFaceShow, &FaceModule::show);
52 
53  CommandDefinition defFaceCreate("face", "create");
54  defFaceCreate
55  .setTitle("create a face")
61  .addArg("congestion-marking-interval", ArgValueType::UNSIGNED, Required::NO, Positional::NO)
62  .addArg("default-congestion-threshold", ArgValueType::UNSIGNED, Required::NO, Positional::NO)
64  parser.addCommand(defFaceCreate, &FaceModule::create);
65 
66  CommandDefinition defFaceDestroy("face", "destroy");
67  defFaceDestroy
68  .setTitle("destroy a face")
70  parser.addCommand(defFaceDestroy, &FaceModule::destroy);
71 }
72 
73 void
75 {
76  auto remoteUri = ctx.args.getOptional<FaceUri>("remote");
77  auto localUri = ctx.args.getOptional<FaceUri>("local");
78  auto uriScheme = ctx.args.getOptional<std::string>("scheme");
79 
80  FaceQueryFilter filter;
81  if (remoteUri) {
82  filter.setRemoteUri(remoteUri->toString());
83  }
84  if (localUri) {
85  filter.setLocalUri(localUri->toString());
86  }
87  if (uriScheme) {
88  filter.setUriScheme(*uriScheme);
89  }
90 
91  FindFace findFace(ctx);
92  FindFace::Code res = findFace.execute(filter, true);
93 
94  ctx.exitCode = static_cast<int>(res);
95  switch (res) {
96  case FindFace::Code::OK:
97  for (const FaceStatus& item : findFace.getResults()) {
98  formatItemText(ctx.out, item, false);
99  ctx.out << '\n';
100  }
101  break;
105  ctx.err << findFace.getErrorReason() << '\n';
106  break;
107  default:
108  BOOST_ASSERT_MSG(false, "unexpected FindFace result");
109  break;
110  }
111 }
112 
113 void
115 {
116  uint64_t faceId = ctx.args.get<uint64_t>("id");
117 
118  FindFace findFace(ctx);
119  FindFace::Code res = findFace.execute(faceId);
120 
121  ctx.exitCode = static_cast<int>(res);
122  switch (res) {
123  case FindFace::Code::OK:
124  formatItemText(ctx.out, findFace.getFaceStatus(), true);
125  break;
128  ctx.err << findFace.getErrorReason() << '\n';
129  break;
130  default:
131  BOOST_ASSERT_MSG(false, "unexpected FindFace result");
132  break;
133  }
134 }
135 
138 static bool
139 persistencyLessThan(FacePersistency x, FacePersistency y)
140 {
141  switch (x) {
142  case FacePersistency::FACE_PERSISTENCY_NONE:
143  return y != FacePersistency::FACE_PERSISTENCY_NONE;
144  case FacePersistency::FACE_PERSISTENCY_ON_DEMAND:
145  return y == FacePersistency::FACE_PERSISTENCY_PERSISTENT ||
146  y == FacePersistency::FACE_PERSISTENCY_PERMANENT;
147  case FacePersistency::FACE_PERSISTENCY_PERSISTENT:
148  return y == FacePersistency::FACE_PERSISTENCY_PERMANENT;
149  case FacePersistency::FACE_PERSISTENCY_PERMANENT:
150  return false;
151  }
152  return static_cast<int>(x) < static_cast<int>(y);
153 }
154 
155 void
157 {
158  auto remoteUri = ctx.args.get<FaceUri>("remote");
159  auto localUri = ctx.args.getOptional<FaceUri>("local");
160  auto persistency = ctx.args.get<FacePersistency>("persistency", FacePersistency::FACE_PERSISTENCY_PERSISTENT);
161  auto lpReliability = ctx.args.getTribool("reliability");
162  auto congestionMarking = ctx.args.getTribool("congestion-marking");
163  auto baseCongestionMarkingIntervalMs = ctx.args.getOptional<uint64_t>("congestion-marking-interval");
164  auto defaultCongestionThreshold = ctx.args.getOptional<uint64_t>("default-congestion-threshold");
165  auto mtuArg = ctx.args.getOptional<std::string>("mtu");
166 
167  // MTU is nominally a uint64_t, but can be the string value 'auto' to unset an override MTU
168  std::optional<uint64_t> mtu;
169  if (mtuArg == "auto") {
170  mtu = std::numeric_limits<uint64_t>::max();
171  }
172  else if (mtuArg) {
173  // boost::lexical_cast<uint64_t> will accept negative numbers
174  int64_t v = -1;
175  if (!boost::conversion::try_lexical_convert<int64_t>(*mtuArg, v) || v < 0) {
176  ctx.exitCode = 2;
177  ctx.err << "MTU must either be a non-negative integer or 'auto'\n";
178  return;
179  }
180 
181  mtu = static_cast<uint64_t>(v);
182  }
183 
184  std::optional<FaceUri> canonicalRemote;
185  std::optional<FaceUri> canonicalLocal;
186 
187  auto updateFace = [&] (ControlParameters respParams, ControlParameters resp) {
188  // faces/update response does not have FaceUris, copy from faces/create response
189  resp.setLocalUri(respParams.getLocalUri())
190  .setUri(respParams.getUri());
191  printSuccess(ctx.out, "face-updated", resp);
192  };
193 
194  auto handle409 = [&] (const ControlResponse& resp) {
195  ControlParameters respParams(resp.getBody());
196  if (respParams.getUri() != canonicalRemote->toString()) {
197  // we are conflicting with a different face, which is a general error
198  return false;
199  }
200 
201  bool isChangingParams = false;
202  ControlParameters params;
203  params.setFaceId(respParams.getFaceId());
204 
205  if (mtu && (!respParams.hasMtu() || respParams.getMtu() != *mtu)) {
206  isChangingParams = true;
207  params.setMtu(*mtu);
208  }
209 
210  if (persistencyLessThan(respParams.getFacePersistency(), persistency)) {
211  isChangingParams = true;
212  params.setFacePersistency(persistency);
213  }
214 
215  if (!boost::logic::indeterminate(lpReliability) &&
216  lpReliability != respParams.getFlagBit(ndn::nfd::BIT_LP_RELIABILITY_ENABLED)) {
217  isChangingParams = true;
218  params.setFlagBit(ndn::nfd::BIT_LP_RELIABILITY_ENABLED, bool(lpReliability));
219  }
220 
221  if (!boost::logic::indeterminate(congestionMarking) &&
222  congestionMarking != respParams.getFlagBit(ndn::nfd::BIT_CONGESTION_MARKING_ENABLED)) {
223  isChangingParams = true;
224  params.setFlagBit(ndn::nfd::BIT_CONGESTION_MARKING_ENABLED, bool(congestionMarking));
225  }
226 
227  if (baseCongestionMarkingIntervalMs) {
228  isChangingParams = true;
229  params.setBaseCongestionMarkingInterval(time::milliseconds(*baseCongestionMarkingIntervalMs));
230  }
231 
232  if (defaultCongestionThreshold) {
233  isChangingParams = true;
234  params.setDefaultCongestionThreshold(*defaultCongestionThreshold);
235  }
236 
237  if (isChangingParams) {
238  ctx.controller.start<ndn::nfd::FaceUpdateCommand>(
239  params,
240  [=, &updateFace] (const auto& cp) { updateFace(respParams, cp); },
241  ctx.makeCommandFailureHandler("updating face"),
242  ctx.makeCommandOptions());
243  }
244  else {
245  // don't do anything
246  printSuccess(ctx.out, "face-exists", respParams);
247  }
248 
249  return true;
250  };
251 
252  auto doCreateFace = [&] {
253  ControlParameters params;
254  params.setUri(canonicalRemote->toString());
255  if (canonicalLocal) {
256  params.setLocalUri(canonicalLocal->toString());
257  }
258  params.setFacePersistency(persistency);
259  if (!boost::logic::indeterminate(lpReliability)) {
260  params.setFlagBit(ndn::nfd::BIT_LP_RELIABILITY_ENABLED, bool(lpReliability));
261  }
262  if (!boost::logic::indeterminate(congestionMarking)) {
263  params.setFlagBit(ndn::nfd::BIT_CONGESTION_MARKING_ENABLED, bool(congestionMarking));
264  }
265  if (baseCongestionMarkingIntervalMs) {
266  params.setBaseCongestionMarkingInterval(time::milliseconds(*baseCongestionMarkingIntervalMs));
267  }
268  if (defaultCongestionThreshold) {
269  params.setDefaultCongestionThreshold(*defaultCongestionThreshold);
270  }
271  if (mtu) {
272  params.setMtu(*mtu);
273  }
274 
275  ctx.controller.start<ndn::nfd::FaceCreateCommand>(
276  params,
277  [&] (const ControlParameters& resp) {
278  printSuccess(ctx.out, "face-created", resp);
279  },
280  [&] (const ControlResponse& resp) {
281  if (resp.getCode() == 409 && handle409(resp)) {
282  return;
283  }
284  ctx.makeCommandFailureHandler("creating face")(resp); // invoke general error handler
285  },
286  ctx.makeCommandOptions());
287  };
288 
289  std::string error;
290  std::tie(canonicalRemote, error) = canonize(ctx, remoteUri);
291  if (canonicalRemote) {
292  // RemoteUri canonization successful
293  if (localUri) {
294  std::tie(canonicalLocal, error) = canonize(ctx, *localUri);
295  if (canonicalLocal) {
296  // LocalUri canonization successful
297  doCreateFace();
298  }
299  else {
300  // LocalUri canonization failure
301  auto canonizationError = canonizeErrorHelper(*localUri, error, "local FaceUri");
302  ctx.exitCode = static_cast<int>(canonizationError.first);
303  ctx.err << canonizationError.second << '\n';
304  }
305  }
306  else {
307  doCreateFace();
308  }
309  }
310  else {
311  // RemoteUri canonization failure
312  auto canonizationError = canonizeErrorHelper(remoteUri, error, "remote FaceUri");
313  ctx.exitCode = static_cast<int>(canonizationError.first);
314  ctx.err << canonizationError.second << '\n';
315  }
316 
317  ctx.face.processEvents();
318 }
319 
320 void
322 {
323  FindFace findFace(ctx);
324  FindFace::Code res = findFace.execute(ctx.args.at("face"));
325 
326  ctx.exitCode = static_cast<int>(res);
327  switch (res) {
328  case FindFace::Code::OK:
329  break;
333  ctx.err << findFace.getErrorReason() << '\n';
334  return;
336  ctx.err << "Multiple faces match specified remote FaceUri. Re-run the command with a FaceId:";
338  ctx.err << '\n';
339  return;
340  default:
341  BOOST_ASSERT_MSG(false, "unexpected FindFace result");
342  return;
343  }
344 
345  const FaceStatus& face = findFace.getFaceStatus();
346 
347  ctx.controller.start<ndn::nfd::FaceDestroyCommand>(
348  ControlParameters().setFaceId(face.getFaceId()),
349  [&] (const ControlParameters& resp) {
350  // We can't use printSuccess because some face attributes come from FaceStatus not ControlResponse
351  ctx.out << "face-destroyed ";
353  ctx.out << ia("id") << face.getFaceId()
354  << ia("local") << face.getLocalUri()
355  << ia("remote") << face.getRemoteUri()
356  << ia("persistency") << face.getFacePersistency();
357  printFaceParams(ctx.out, ia, resp);
358  },
359  ctx.makeCommandFailureHandler("destroying face"),
360  ctx.makeCommandOptions());
361 
362  ctx.face.processEvents();
363 }
364 
365 void
366 FaceModule::fetchStatus(ndn::nfd::Controller& controller,
367  const std::function<void()>& onSuccess,
368  const ndn::nfd::DatasetFailureCallback& onFailure,
369  const CommandOptions& options)
370 {
371  controller.fetch<ndn::nfd::FaceDataset>(
372  [this, onSuccess] (const auto& result) {
373  m_status = result;
374  onSuccess();
375  },
376  onFailure, options);
377 }
378 
379 void
380 FaceModule::formatStatusXml(std::ostream& os) const
381 {
382  os << "<faces>";
383  for (const FaceStatus& item : m_status) {
384  this->formatItemXml(os, item);
385  }
386  os << "</faces>";
387 }
388 
389 void
390 FaceModule::formatItemXml(std::ostream& os, const FaceStatus& item) const
391 {
392  os << "<face>";
393 
394  os << "<faceId>" << item.getFaceId() << "</faceId>";
395  os << "<remoteUri>" << xml::Text{item.getRemoteUri()} << "</remoteUri>";
396  os << "<localUri>" << xml::Text{item.getLocalUri()} << "</localUri>";
397 
398  if (item.hasExpirationPeriod()) {
399  os << "<expirationPeriod>" << xml::formatDuration(item.getExpirationPeriod())
400  << "</expirationPeriod>";
401  }
402  os << "<faceScope>" << item.getFaceScope() << "</faceScope>";
403  os << "<facePersistency>" << item.getFacePersistency() << "</facePersistency>";
404  os << "<linkType>" << item.getLinkType() << "</linkType>";
405 
406  if (!item.hasBaseCongestionMarkingInterval() && !item.hasDefaultCongestionThreshold()) {
407  os << "<congestion/>";
408  }
409  else {
410  os << "<congestion>";
411  if (item.hasBaseCongestionMarkingInterval()) {
412  os << "<baseMarkingInterval>" << xml::formatDuration(item.getBaseCongestionMarkingInterval())
413  << "</baseMarkingInterval>";
414  }
415  if (item.hasDefaultCongestionThreshold()) {
416  os << "<defaultThreshold>" << item.getDefaultCongestionThreshold() << "</defaultThreshold>";
417  }
418  os << "</congestion>";
419  }
420 
421  if (item.hasMtu()) {
422  os << "<mtu>" << item.getMtu() << "</mtu>";
423  }
424 
425  if (item.getFlags() == 0) {
426  os << "<flags/>";
427  }
428  else {
429  os << "<flags>";
430  os << xml::Flag{"localFieldsEnabled", item.getFlagBit(ndn::nfd::BIT_LOCAL_FIELDS_ENABLED)};
431  os << xml::Flag{"lpReliabilityEnabled", item.getFlagBit(ndn::nfd::BIT_LP_RELIABILITY_ENABLED)};
432  os << xml::Flag{"congestionMarkingEnabled", item.getFlagBit(ndn::nfd::BIT_CONGESTION_MARKING_ENABLED)};
433  os << "</flags>";
434  }
435 
436  os << "<packetCounters>";
437  os << "<incomingPackets>"
438  << "<nInterests>" << item.getNInInterests() << "</nInterests>"
439  << "<nData>" << item.getNInData() << "</nData>"
440  << "<nNacks>" << item.getNInNacks() << "</nNacks>"
441  << "</incomingPackets>";
442  os << "<outgoingPackets>"
443  << "<nInterests>" << item.getNOutInterests() << "</nInterests>"
444  << "<nData>" << item.getNOutData() << "</nData>"
445  << "<nNacks>" << item.getNOutNacks() << "</nNacks>"
446  << "</outgoingPackets>";
447  os << "</packetCounters>";
448 
449  os << "<byteCounters>";
450  os << "<incomingBytes>" << item.getNInBytes() << "</incomingBytes>";
451  os << "<outgoingBytes>" << item.getNOutBytes() << "</outgoingBytes>";
452  os << "</byteCounters>";
453 
454  os << "</face>";
455 }
456 
457 void
458 FaceModule::formatStatusText(std::ostream& os) const
459 {
460  os << "Faces:\n";
461  for (const FaceStatus& item : m_status) {
462  os << " ";
463  formatItemText(os, item, false);
464  os << '\n';
465  }
466 }
467 
468 void
469 FaceModule::formatItemText(std::ostream& os, const FaceStatus& item, bool wantMultiLine)
470 {
471  text::ItemAttributes ia(wantMultiLine, 10);
472 
473  os << ia("faceid") << item.getFaceId();
474  os << ia("remote") << item.getRemoteUri();
475  os << ia("local") << item.getLocalUri();
476 
477  if (item.hasExpirationPeriod()) {
478  os << ia("expires") << text::formatDuration<time::seconds>(item.getExpirationPeriod());
479  }
480 
481  if (item.hasBaseCongestionMarkingInterval() || item.hasDefaultCongestionThreshold()) {
482  os << ia("congestion") << "{";
483  text::Separator congestionSep("", " ");
484  if (item.hasBaseCongestionMarkingInterval()) {
485  os << congestionSep << "base-marking-interval="
486  << text::formatDuration<time::milliseconds>(item.getBaseCongestionMarkingInterval());
487  }
488  if (item.hasDefaultCongestionThreshold()) {
489  os << congestionSep << "default-threshold=" << item.getDefaultCongestionThreshold() << "B";
490  }
491  os << "}";
492  }
493 
494  if (item.hasMtu()) {
495  os << ia("mtu") << item.getMtu();
496  }
497 
498  os << ia("counters")
499  << "{in={"
500  << item.getNInInterests() << "i "
501  << item.getNInData() << "d "
502  << item.getNInNacks() << "n "
503  << item.getNInBytes() << "B} "
504  << "out={"
505  << item.getNOutInterests() << "i "
506  << item.getNOutData() << "d "
507  << item.getNOutNacks() << "n "
508  << item.getNOutBytes() << "B}}";
509 
510  os << ia("flags") << '{';
511  text::Separator flagSep("", " ");
512  os << flagSep << item.getFaceScope();
513  os << flagSep << item.getFacePersistency();
514  os << flagSep << item.getLinkType();
515  if (item.getFlagBit(ndn::nfd::BIT_LOCAL_FIELDS_ENABLED)) {
516  os << flagSep << "local-fields";
517  }
518  if (item.getFlagBit(ndn::nfd::BIT_LP_RELIABILITY_ENABLED)) {
519  os << flagSep << "lp-reliability";
520  }
521  if (item.getFlagBit(ndn::nfd::BIT_CONGESTION_MARKING_ENABLED)) {
522  os << flagSep << "congestion-marking";
523  }
524  os << '}';
525 
526  os << ia.end();
527 }
528 
529 void
530 FaceModule::printSuccess(std::ostream& os,
531  const std::string& actionSummary,
532  const ControlParameters& resp)
533 {
535  os << actionSummary << ' '
536  << ia("id") << resp.getFaceId()
537  << ia("local") << resp.getLocalUri()
538  << ia("remote") << resp.getUri()
539  << ia("persistency") << resp.getFacePersistency();
540  printFaceParams(os, ia, resp);
541 }
542 
543 void
544 FaceModule::printFaceParams(std::ostream& os, text::ItemAttributes& ia, const ControlParameters& resp)
545 {
546  os << ia("reliability") << text::OnOff{resp.getFlagBit(ndn::nfd::BIT_LP_RELIABILITY_ENABLED)}
547  << ia("congestion-marking") << text::OnOff{resp.getFlagBit(ndn::nfd::BIT_CONGESTION_MARKING_ENABLED)};
548  if (resp.hasBaseCongestionMarkingInterval()) {
549  os << ia("congestion-marking-interval")
550  << text::formatDuration<time::milliseconds>(resp.getBaseCongestionMarkingInterval());
551  }
552  if (resp.hasDefaultCongestionThreshold()) {
553  os << ia("default-congestion-threshold") << resp.getDefaultCongestionThreshold() << "B";
554  }
555  if (resp.hasMtu()) {
556  os << ia("mtu") << resp.getMtu();
557  }
558  os << '\n';
559 }
560 
561 } // namespace nfd::tools::nfdc
std::optional< T > getOptional(std::string_view key) const
T get(std::string_view key, const T &defaultValue=T()) const
boost::logic::tribool getTribool(std::string_view key) const
Get an optional boolean argument as tribool.
CommandDefinition & setTitle(std::string_view title)
Set one-line description.
CommandDefinition & addArg(const std::string &name, ArgValueType valueType, Required isRequired=Required::NO, Positional allowPositional=Positional::NO, const std::string &metavar="")
Declare an argument.
CommandParser & addCommand(const CommandDefinition &def, const ExecuteCommand &execute, std::underlying_type_t< AvailableIn > modes=AVAILABLE_IN_ALL)
Add an available command.
CommandParser & addAlias(const std::string &noun, const std::string &verb, const std::string &verb2)
Add an alias "noun verb2" to existing command "noun verb".
Context for command execution.
std::ostream & out
output stream
ndn::nfd::Controller & controller
const CommandArguments & args
ndn::nfd::CommandFailureCallback makeCommandFailureHandler(const std::string &commandName)
ndn::nfd::CommandOptions makeCommandOptions() const
std::ostream & err
error stream
static void printFaceParams(std::ostream &os, text::ItemAttributes &ia, const ControlParameters &resp)
Print face response parameters to specified ostream.
void formatItemXml(std::ostream &os, const FaceStatus &item) const
Format a single status item as XML.
void formatStatusText(std::ostream &os) const override
Format collected status as text.
static void destroy(ExecuteContext &ctx)
The 'face destroy' command.
static void registerCommands(CommandParser &parser)
Register 'face list', 'face show', 'face create', 'face destroy' commands.
Definition: face-module.cpp:36
void formatStatusXml(std::ostream &os) const override
Format collected status as XML.
void fetchStatus(ndn::nfd::Controller &controller, const std::function< void()> &onSuccess, const ndn::nfd::DatasetFailureCallback &onFailure, const CommandOptions &options) override
Collect status from NFD.
static void formatItemText(std::ostream &os, const FaceStatus &item, bool wantMultiLine)
Format a single status item as text.
static void printSuccess(std::ostream &os, const std::string &actionSummary, const ControlParameters &resp)
Print face action success message to specified ostream.
static void list(ExecuteContext &ctx)
The 'face list' command.
Definition: face-module.cpp:74
static void show(ExecuteContext &ctx)
The 'face show' command.
static void create(ExecuteContext &ctx)
The 'face create' command.
Procedure to find a face.
const FaceStatus & getFaceStatus() const
@ AMBIGUOUS
found multiple faces and allowMulti is false
@ CANONIZE_ERROR
error during FaceUri canonization
@ OK
found exactly one face, or found multiple faces when allowMulti is true
Code execute(const FaceUri &faceUri, bool allowMulti=false)
Find face by FaceUri.
const std::vector< FaceStatus > & getResults() const
const std::string & getErrorReason() const
void printDisambiguation(std::ostream &os, DisambiguationStyle style) const
Print results for disambiguation.
Print attributes of an item.
Print different string on first and subsequent usage.
std::string formatDuration(time::nanoseconds d)
std::pair< FindFace::Code, std::string > canonizeErrorHelper(const FaceUri &uri, const std::string &error, const std::string &field)
Helper to generate exit code and error message for face canonization failures.
@ YES
argument is required
@ NO
argument is optional
std::pair< std::optional< FaceUri >, std::string > canonize(ExecuteContext &ctx, const FaceUri &uri)
Canonize a FaceUri.
static bool persistencyLessThan(FacePersistency x, FacePersistency y)
Order persistency in NONE < ON_DEMAND < PERSISTENCY < PERMANENT.
@ YES
argument can be specified as positional
@ NO
argument must be named
@ FACE_ID_OR_URI
FaceId or FaceUri.
@ UNSIGNED
Non-negative integer.
@ FACE_PERSISTENCY
Face persistency 'persistent' or 'permanent'.
Print boolean as 'on' or 'off'.
Print true as an empty element and false as nothing.