OpenCPN Partial API docs
udev_rule_mgr.cpp
Go to the documentation of this file.
1 
2 /**************************************************************************
3  * Copyright (C) 2021 Alec Leamas *
4  * *
5  * This program is free software; you can redistribute it and/or modify *
6  * it under the terms of the GNU General Public License as published by *
7  * the Free Software Foundation; either version 2 of the License, or *
8  * (at your option) any later version. *
9  * *
10  * This program is distributed in the hope that it will be useful, *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13  * GNU General Public License for more details. *
14  * *
15  * You should have received a copy of the GNU General Public License *
16  * along with this program; if not, write to the *
17  * Free Software Foundation, Inc., *
18  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
19  ***************************************************************************/
22 #include "config.h"
23 
24 #include <algorithm>
25 #include <cassert>
26 #include <sstream>
27 #include <vector>
28 
29 #include <stdlib.h>
30 
31 #include <wx/button.h>
32 #include <wx/checkbox.h>
33 #include <wx/dcclient.h>
34 #include <wx/dialog.h>
35 #include <wx/frame.h>
36 #include <wx/panel.h>
37 #include <wx/sizer.h>
38 #include <wx/statline.h>
39 #include <wx/stattext.h>
40 #include <wx/textctrl.h>
41 
42 #include "model/linux_devices.h"
43 #include "model/logger.h"
44 #include "model/ocpn_utils.h"
45 
46 #include "gui_lib.h"
47 #include "udev_rule_mgr.h"
48 
49 
50 #if !defined(__linux__) || defined(__ANDROID__)
51 
52 // non-linux platforms: Empty place holders.
53 bool CheckDongleAccess(wxWindow* parent) { return true; }
54 bool CheckSerialAccess(wxWindow* parent, const std::string device) {
55  return true;
56 }
58 
59 
60 #else
61 
62 static bool hide_dongle_dialog;
63 static bool hide_device_dialog;
64 
65 static const char* const DONGLE_INTRO = _(R"(
66 An OpenCPN dongle is detected but cannot be used due to missing permissions.
67 
68 This problem can be fixed by installing a udev rules file. Once installed,
69 it will ensure that the dongle permissions are OK.
70 )");
71 
72 static const char* const FLATPAK_INTRO_TRAILER = _(R"(
73 
74 On flatpak, this must be done using the manual command instructions below
75 )");
76 
77 static const char* const DEVICE_INTRO = _(R"(
78 The device @DEVICE@ exists but cannot be used due to missing permissions.
79 
80 This problem can be fixed by installing a udev rules file. Once installed,
81 the rules file will fix the permissions problem.
82 )");
83 
84 static const char* const DEVICE_LINK_INTRO = _(R"(
85 
86 It will also create a new device called @SYMLINK@. It is recommended to use
87 @SYMLINK@ instead of @DEVICE@ to avoid problems with changing device names,
88 in particular on laptops.
89 )");
90 
91 static const char* const HIDE_DIALOG_LABEL =
92  _("Do not show this dialog next time");
93 
94 static const char* const RULE_SUCCESS_TTYS_MSG = _(R"(
95 Rule successfully installed. To activate the new rule restart the system.
96 )");
97 
98 static const char* const RULE_SUCCESS_MSG = _(R"(
99 Rule successfully installed. To activate the new rule restart system or:
100 - Exit opencpn.
101 - Unplug and re-insert the USB device.
102 - Restart opencpn
103 )");
104 
105 static const char* const FLATPAK_INSTALL_MSG = _(R"(
106 To do after installing the rule according to instructions:
107 - Exit opencpn.
108 - Unplug and re-insert the USB device.
109 - Restart opencpn
110 )");
111 
112 static const char* const DEVICE_NOT_FOUND =
113  _("The device @device@ can not be found (disconnected?)");
114 
115 static const char* const INSTRUCTIONS = "@pkexec@ cp @PATH@ /etc/udev/rules.d";
116 
118 class DeviceNotFoundDlg : public wxFrame {
119 public:
121  static void Create(wxWindow* parent, const std::string& device) {
122  wxWindow* dlg = new DeviceNotFoundDlg(parent, device);
123  dlg->Show();
124  }
125 
127  static void DestroyOpenWindows() {
128  for (const auto& name : open_windows) {
129  auto window = wxWindow::FindWindowByName(name);
130  if (window) window->Destroy();
131  }
132  open_windows.clear();
133  }
134 
135 private:
136  static std::vector<std::string> open_windows;
137 
138  class ButtonsSizer : public wxStdDialogButtonSizer {
139  public:
140  ButtonsSizer(DeviceNotFoundDlg* parent) : wxStdDialogButtonSizer() {
141  auto button = new wxButton(parent, wxID_OK);
142  AddButton(button);
143  Realize();
144  }
145  };
146 
147  DeviceNotFoundDlg(wxWindow* parent, const std::string& device)
148  : wxFrame(parent, wxID_ANY, _("Opencpn: device not found"),
149  wxDefaultPosition, wxDefaultSize,
150  wxDEFAULT_FRAME_STYLE | wxFRAME_FLOAT_ON_PARENT) {
151  std::stringstream ss;
152  ss << "dlg-id-" << rand();
153  SetName(ss.str());
154  open_windows.push_back(ss.str());
155 
156  Bind(wxEVT_CLOSE_WINDOW, [&](wxCloseEvent& e) {
157  OnClose();
158  e.Skip();
159  });
160  Bind(wxEVT_COMMAND_BUTTON_CLICKED, [&](wxCommandEvent&) { OnClose(); });
161 
162  auto vbox = new wxBoxSizer(wxVERTICAL);
163  SetSizer(vbox);
164  auto flags = wxSizerFlags().Expand().Border();
165  std::string txt(DEVICE_NOT_FOUND);
166  ocpn::replace(txt, "@device@", device);
167  vbox->Add(0, 0, 1); // vertical space
168  vbox->Add(new wxStaticText(this, wxID_ANY, txt), flags);
169  vbox->Add(0, 0, 1);
170  vbox->Add(new wxStaticLine(this), wxSizerFlags().Expand());
171  vbox->Add(new ButtonsSizer(this), flags);
172  Layout();
173  CenterOnScreen();
174  SetFocus();
175  }
176 
177  void OnClose() {
178  const std::string name(GetName().ToStdString());
179  auto found =
180  std::find_if(open_windows.begin(), open_windows.end(),
181  [name](const std::string& s) { return s == name; });
182  assert(found != std::end(open_windows) &&
183  "Cannot find dialog in window list");
184  open_windows.erase(found);
185  Destroy();
186  }
187 };
188 
189 std::vector<std::string> DeviceNotFoundDlg::open_windows;
190 
191 void DestroyDeviceNotFoundDialogs() { DeviceNotFoundDlg::DestroyOpenWindows(); }
192 
194 class HideCheckbox : public wxCheckBox {
195 public:
196  HideCheckbox(wxWindow* parent, const char* label, bool* state)
197  : wxCheckBox(parent, wxID_ANY, label, wxDefaultPosition, wxDefaultSize,
198  wxALIGN_LEFT),
199  m_state(state) {
200  SetValue(*state);
201  Bind(wxEVT_CHECKBOX,
202  [&](wxCommandEvent& ev) { *m_state = ev.IsChecked(); });
203  }
204 
205 private:
206  bool* m_state;
207 };
208 
210 class HidePanel : public wxPanel {
211 public:
212  HidePanel(wxWindow* parent, const char* label, bool* state)
213  : wxPanel(parent) {
214  auto hbox = new wxBoxSizer(wxHORIZONTAL);
215  hbox->Add(new HideCheckbox(this, label, state), wxSizerFlags().Expand());
216  SetSizer(hbox);
217  Fit();
218  Show();
219  }
220 };
221 
223 class HideShowPanel : public wxPanel {
224 public:
225  HideShowPanel(wxWindow* parent, wxWindow* child)
226  : wxPanel(parent), m_show(true), m_child(child) {
227  m_arrow = new wxStaticText(this, wxID_ANY, "");
228  m_arrow->Bind(wxEVT_LEFT_DOWN, [&](wxMouseEvent& ev) { Toggle(); });
229  if (m_child) {
230  Toggle();
231  }
232  }
233 
234 protected:
235  bool m_show;
236  wxWindow* m_child;
237  wxStaticText* m_arrow;
238 
239  void Toggle() {
240  static const auto ARROW_DOWN = L"\u25BC";
241  static const auto ARROW_RIGHT = L"\u25BA";
242 
243  m_show = !m_show;
244  m_child->Show(m_show);
245  m_arrow->SetLabel(std::string(" ") + (m_show ? ARROW_DOWN : ARROW_RIGHT));
246  GetGrandParent()->Fit();
247  GetGrandParent()->Layout();
248  }
249 };
250 
252 class ManualInstructions : public HideShowPanel {
253 public:
254  ManualInstructions(wxWindow* parent, const char* cmd)
255  : HideShowPanel(parent, 0) {
256  m_child = GetCmd(parent, cmd);
257  Toggle();
258  auto flags = wxSizerFlags().Expand();
259 
260  auto hbox = new wxBoxSizer(wxHORIZONTAL);
261  const char* label = _("Manual command line instructions");
262  hbox->Add(new wxStaticText(this, wxID_ANY, label), flags);
263  hbox->Add(m_arrow);
264 
265  auto vbox = new wxBoxSizer(wxVERTICAL);
266 
267  vbox->Add(hbox);
268  flags = flags.Border(wxLEFT);
269  vbox->Add(m_child, flags.ReserveSpaceEvenIfHidden());
270 
271  SetSizer(vbox);
272  SetAutoLayout(true);
273  Show();
274  }
275 
276 private:
277  wxTextCtrl* GetCmd(wxWindow* parent, const char* tmpl) {
278  std::string cmd(tmpl);
279  ocpn::replace(cmd, "@PATH@", GetDongleRule());
280  auto ctrl = new CopyableText(this, cmd.c_str());
281  ctrl->SetMinSize(parent->GetTextExtent(cmd + "aaa"));
282  return ctrl;
283  }
284  wxWindow* m_parent;
285 };
286 
288 class ReviewRule : public HideShowPanel {
289 public:
290  ReviewRule(wxWindow* parent, const std::string& rule)
291  : HideShowPanel(parent, 0) {
292  int from = rule[0] == '\n' ? 1 : 0;
293  m_child = new wxStaticText(this, wxID_ANY, rule.substr(from));
294  Toggle();
295 
296  auto flags = wxSizerFlags().Expand();
297  auto hbox = new wxBoxSizer(wxHORIZONTAL);
298  hbox->Add(new wxStaticText(this, wxID_ANY, _("Review rule")), flags);
299  hbox->Add(m_arrow);
300 
301  auto vbox = new wxBoxSizer(wxVERTICAL);
302  vbox->Add(hbox);
303  auto indent = parent->GetTextExtent("ABCDE").GetWidth();
304  flags = flags.Border(wxLEFT, indent);
305  vbox->Add(m_child, flags.ReserveSpaceEvenIfHidden());
306  SetSizer(vbox);
307  SetAutoLayout(true);
308  Show();
309  }
310 };
311 
313 static std::string GetRule(const std::string& path) {
314  std::ifstream input(path.c_str());
315  std::ostringstream buf;
316  buf << input.rdbuf();
317  input.close();
318  if (input.bad()) {
319  WARNING_LOG << "Cannot open rule file: " << path;
320  }
321  return buf.str();
322 }
323 
325 class DongleInfoPanel : public wxPanel {
326 public:
327  DongleInfoPanel(wxWindow* parent) : wxPanel(parent) {
328  std::string cmd(INSTRUCTIONS);
329  std::string rule_path(GetDongleRule());
330  ocpn::replace(cmd, "@PATH@", rule_path.c_str());
331  ocpn::replace(cmd, "@pkexec@", "sudo");
332  auto vbox = new wxBoxSizer(wxVERTICAL);
333  vbox->Add(new ManualInstructions(this, cmd.c_str()));
334  std::string rule_text = GetRule(rule_path);
335  vbox->Add(new ReviewRule(this, rule_text.c_str()));
336  SetAutoLayout(true);
337  SetSizer(vbox);
338  }
339 };
340 
342 class DeviceInfoPanel : public wxPanel {
343 public:
344  DeviceInfoPanel(wxWindow* parent, const std::string rule_path)
345  : wxPanel(parent) {
346  std::string cmd(INSTRUCTIONS);
347  ocpn::replace(cmd, "@PATH@", rule_path.c_str());
348  ocpn::replace(cmd, "@pkexec@", "sudo");
349  auto vbox = new wxBoxSizer(wxVERTICAL);
350  vbox->Add(new ManualInstructions(this, cmd.c_str()));
351  vbox->Add(new ReviewRule(this, GetRule(rule_path)));
352  SetAutoLayout(true);
353  SetSizer(vbox);
354  }
355 };
356 
358 class Buttons : public wxPanel {
359 public:
360  Buttons(wxWindow* parent, const char* rule_path)
361  : wxPanel(parent), m_rule_path(rule_path) {
362  auto sizer = new wxBoxSizer(wxHORIZONTAL);
363  auto flags = wxSizerFlags().Bottom().Border(wxLEFT);
364  sizer->Add(1, 1, 100, wxEXPAND); // Expanding spacer
365  auto install = new wxButton(this, wxID_ANY, _("Install rule"));
366  install->Bind(wxEVT_COMMAND_BUTTON_CLICKED,
367  [&](wxCommandEvent& ev) { DoInstall(); });
368  install->Enable(getenv("FLATPAK_ID") == NULL);
369  sizer->Add(install, flags);
370  auto quit = new wxButton(this, wxID_EXIT, _("Quit"));
371  quit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [&](wxCommandEvent& ev) {
372  if (getenv("FLATPAK_ID")) {
373  auto flags = wxOK | wxICON_INFORMATION;
374  auto msg = FLATPAK_INSTALL_MSG;
375  OCPNMessageBox(this, msg, _("OpenCPN"), flags);
376  }
377  dynamic_cast<wxDialog*>(GetParent())->EndModal(0);
378  });
379  sizer->Add(quit, flags);
380  SetSizer(sizer);
381  Fit();
382  Show();
383  }
384 
385  void DoInstall() {
386  using namespace std;
387  string cmd(INSTRUCTIONS);
388  ocpn::replace(cmd, "@PATH@", m_rule_path);
389  ocpn::replace(cmd, "@pkexec@", "sudo");
390  ifstream f(m_rule_path);
391  auto rule =
392  string(istreambuf_iterator<char>(f), istreambuf_iterator<char>());
393  int sts = system(cmd.c_str());
394  int flags = wxOK | wxICON_WARNING;
395  const char* msg = _("Errors encountered installing rule.");
396  if (WIFEXITED(sts) && WEXITSTATUS(sts) == 0) {
397  if (rule.find("ttyS") != std::string::npos) {
398  msg = RULE_SUCCESS_TTYS_MSG;
399  } else {
400  msg = RULE_SUCCESS_MSG;
401  }
402  flags = wxOK | wxICON_INFORMATION;
403  }
404  OCPNMessageBox(this, msg, _("OpenCPN Info"), flags);
405  }
406 
407 private:
408  std::string m_rule_path;
409 };
410 
412 class DongleRuleDialog : public wxDialog {
413 public:
414  DongleRuleDialog(wxWindow* parent)
415  : wxDialog(parent, wxID_ANY, _("Manage dongle udev rule")) {
416  auto sizer = new wxBoxSizer(wxVERTICAL);
417  auto flags = wxSizerFlags().Expand().Border();
418  std::string intro(DONGLE_INTRO);
419  if (getenv("FLATPAK_ID")) {
420  intro += FLATPAK_INTRO_TRAILER;
421  }
422  sizer->Add(new wxStaticText(this, wxID_ANY, intro), flags);
423  sizer->Add(new wxStaticLine(this), flags);
424  sizer->Add(new DongleInfoPanel(this), flags);
425  sizer->Add(new HidePanel(this, HIDE_DIALOG_LABEL, &hide_dongle_dialog),
426  flags.Left());
427  sizer->Add(new wxStaticLine(this), flags);
428  sizer->Add(new Buttons(this, GetDongleRule().c_str()), flags);
429  SetSizer(sizer);
430  SetAutoLayout(true);
431  Fit();
432  }
433 };
434 
436 static std::string GetDeviceIntro(const char* device, std::string symlink) {
437  std::string intro(DEVICE_INTRO);
438 
439  std::string dev_name(device);
440  ocpn::replace(dev_name, "/dev/", "");
441  if (!ocpn::startswith(dev_name, "ttyS")) {
442  intro += DEVICE_LINK_INTRO;
443  }
444  if (getenv("FLATPAK_ID")) {
445  intro += FLATPAK_INTRO_TRAILER;
446  }
447  ocpn::replace(symlink, "/dev/", "");
448  while (intro.find("@SYMLINK@") != std::string::npos) {
449  ocpn::replace(intro, "@SYMLINK@", symlink);
450  }
451  while (intro.find("@DEVICE@") != std::string::npos) {
452  ocpn::replace(intro, "@DEVICE@", dev_name.c_str());
453  }
454  return intro;
455 }
456 
458 class DeviceRuleDialog : public wxDialog {
459 public:
460  DeviceRuleDialog(wxWindow* parent, const char* device_path)
461  : wxDialog(parent, wxID_ANY, _("Manage device udev rule")) {
462  auto sizer = new wxBoxSizer(wxVERTICAL);
463  auto flags = wxSizerFlags().Expand().Border();
464 
465  std::string symlink(MakeUdevLink());
466  auto intro = GetDeviceIntro(device_path, symlink.c_str());
467  auto rule_path = GetDeviceRule(device_path, symlink.c_str());
468  sizer->Add(new wxStaticText(this, wxID_ANY, intro), flags);
469  sizer->Add(new wxStaticLine(this), flags);
470  sizer->Add(new DeviceInfoPanel(this, rule_path), flags);
471  sizer->Add(new HidePanel(this, HIDE_DIALOG_LABEL, &hide_device_dialog),
472  flags);
473  sizer->Add(new wxStaticLine(this), flags);
474  sizer->Add(new Buttons(this, rule_path.c_str()), flags);
475 
476  SetSizer(sizer);
477  SetAutoLayout(true);
478  Fit();
479  }
480 };
481 
482 bool CheckSerialAccess(wxWindow* parent, const std::string device) {
483  if (hide_device_dialog) {
484  return true;
485  }
486  if (!ocpn::exists(device)) {
487  DeviceNotFoundDlg::Create(parent, device);
488  return false;
489  }
490  int result = 0;
491  if (!IsDevicePermissionsOk(device.c_str())) {
492  auto dialog = new DeviceRuleDialog(parent, device.c_str());
493  result = dialog->ShowModal();
494  delete dialog;
495  }
496  return result == 0;
497 }
498 
499 bool CheckDongleAccess(wxWindow* parent) {
500  int result = 0;
501  if (IsDonglePermissionsWrong() && !hide_dongle_dialog) {
502  auto dialog = new DongleRuleDialog(parent);
503  result = dialog->ShowModal();
504  delete dialog;
505  }
506  return result == 0;
507 }
508 
509 #endif // !defined(__linux__) || defined(__ANDROID__)
The Done button.
Non-editable TextCtrl, used like wxStaticText but is copyable.
Definition: gui_lib.h:34
General purpose GUI support.
std::string MakeUdevLink()
Get next available udev rule base name.
bool IsDevicePermissionsOk(const char *path)
Check device path permissions.
std::string GetDongleRule()
std::string GetDeviceRule(const char *device, const char *symlink)
Get device udev rule.
bool IsDonglePermissionsWrong()
Return true if an existing dongle cannot be accessed.
Low level udev usb device management.
void DestroyDeviceNotFoundDialogs()
Destroy all open "Device not found" dialog windows.
bool CheckDongleAccess(wxWindow *parent)
Runs checks and if required dialogs to make dongle accessible.
bool CheckSerialAccess(wxWindow *parent, const std::string device)
Run checks and possible dialogs to ensure device is accessible.
Access checks for comm devices and dongle.