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