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