OpenCPN Partial API docs
Loading...
Searching...
No Matches
udev_rule_mgr.cpp
1/******************************************************************************
2 *
3 * Project: OpenCPN
4 *
5 ***************************************************************************
6 * Copyright (C) 2021 Alec Leamas *
7 * *
8 * This program is free software; you can redistribute it and/or modify *
9 * it under the terms of the GNU General Public License as published by *
10 * the Free Software Foundation; either version 2 of the License, or *
11 * (at your option) any later version. *
12 * *
13 * This program is distributed in the hope that it will be useful, *
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16 * GNU General Public License for more details. *
17 * *
18 * You should have received a copy of the GNU General Public License *
19 * along with this program; if not, write to the *
20 * Free Software Foundation, Inc., *
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
22 ***************************************************************************
23 */
24
25#include "config.h"
26
27#include <wx/button.h>
28#include <wx/checkbox.h>
29#include <wx/dcclient.h>
30#include <wx/dialog.h>
31#include <wx/frame.h>
32#include <wx/panel.h>
33#include <wx/sizer.h>
34#include <wx/statline.h>
35#include <wx/stattext.h>
36#include <wx/textctrl.h>
37
38#include "udev_rule_mgr.h"
39#include "linux_devices.h"
40#include "logger.h"
41#include "gui_lib.h"
42#include "ocpn_utils.h"
43
44static bool hide_dongle_dialog;
45static bool hide_device_dialog;
46
47static const char* const DONGLE_INTRO = _(R"""(
48An OpenCPN dongle is detected but cannot be used due to missing permissions.
49
50This problem can be fixed by installing a udev rules file. Once installed,
51it will ensure that the dongle permissions are OK.
52)""");
53
54static const char* const FLATPAK_INTRO_TRAILER = _(R"""(
55
56On flatpak, this must be done using the manual command instructions below
57)""");
58
59static const char* const DEVICE_INTRO = _(R"""(
60The device @DEVICE@ exists but cannot be used due to missing permissions.
61
62This problem can be fixed by installing a udev rules file. Once installed,
63the rules file will fix the permissions problem.
64
65It will also create a new device called @SYMLINK@. It is recommended to use
66@SYMLINK@ instead of @DEVICE@ to avoid problems with changing device names,
67in particular on laptops.
68)""");
69
70static const char* const HIDE_DIALOG_LABEL =
71 _("Do not show this dialog next time");
72
73static const char* const RULE_SUCCESS_MSG = _(R"""(
74Rule successfully installed. To activate the new rule:
75- Exit opencpn.
76- Unplug and re-insert the USB device.
77- Restart opencpn
78)""");
79
80static const char* const FLATPAK_INSTALL_MSG = _(R"""(
81To do after installing the rule according to instructions:
82- Exit opencpn.
83- Unplug and re-insert the USB device.
84- Restart opencpn
85)""");
86
87static const char* const DEVICE_NOT_FOUND =
88 _("The device @device@ can not be found (disconnected?)");
89
91struct HideCheckbox : public wxCheckBox {
92 HideCheckbox(wxWindow* parent, const char* label, bool* state)
93 : wxCheckBox(parent, wxID_ANY, label, wxDefaultPosition, wxDefaultSize,
94 wxALIGN_RIGHT),
95 m_state(state) {
96 SetValue(*state);
97 Bind(wxEVT_CHECKBOX,
98 [&](wxCommandEvent& ev) { *m_state = ev.IsChecked(); });
99 }
100
101private:
102 bool* m_state;
103};
104
106struct HidePanel : wxPanel {
107 HidePanel(wxWindow* parent, const char* label, bool* state)
108 : wxPanel(parent) {
109 auto hbox = new wxBoxSizer(wxHORIZONTAL);
110 hbox->Add(1, 1, 100, wxEXPAND); // Expanding spacer
111 hbox->Add(new HideCheckbox(this, label, state), wxSizerFlags().Expand());
112 SetSizer(hbox);
113 Fit();
114 Show();
115 }
116};
117
118static const char* const INSTRUCTIONS = "@pkexec@ cp @PATH@ /etc/udev/rules.d";
119
121class HideShowPanel : public wxPanel {
122public:
123 HideShowPanel(wxWindow* parent, wxWindow* child)
124 : wxPanel(parent), m_show(true), m_child(child) {
125 m_arrow = new wxStaticText(this, wxID_ANY, "");
126 m_arrow->Bind(wxEVT_LEFT_DOWN, [&](wxMouseEvent& ev) { toggle(); });
127 if (m_child) {
128 toggle();
129 }
130 }
131
132protected:
133 bool m_show;
134 wxWindow* m_child;
135 wxStaticText* m_arrow;
136
137 void toggle() {
138 static const auto ARROW_DOWN = L"\u25BC";
139 static const auto ARROW_RIGHT = L"\u25BA";
140
141 m_show = !m_show;
142 m_child->Show(m_show);
143 m_arrow->SetLabel(m_show ? ARROW_DOWN : ARROW_RIGHT);
144 GetGrandParent()->Fit();
145 GetGrandParent()->Layout();
146 }
147};
148
151public:
152 ManualInstructions(wxWindow* parent, const char* cmd)
153 : HideShowPanel(parent, 0) {
154 m_child = get_cmd(parent, cmd);
155 toggle();
156 auto flags = wxSizerFlags().Expand().Border().Right();
157
158 auto hbox = new wxBoxSizer(wxHORIZONTAL);
159 const char* label = _("Manual command line instructions");
160 hbox->Add(new wxStaticText(this, wxID_ANY, label), flags);
161 hbox->Add(m_arrow);
162
163 auto vbox = new wxBoxSizer(wxVERTICAL);
164 vbox->Add(hbox);
165 auto indent = parent->GetTextExtent("aaa").GetWidth();
166 flags = flags.Border(wxLEFT, indent);
167 vbox->Add(m_child, flags.ReserveSpaceEvenIfHidden());
168
169 SetSizer(vbox);
170 SetAutoLayout(true);
171 Show();
172 }
173
174private:
175 wxTextCtrl* get_cmd(wxWindow* parent, const char* tmpl) {
176 std::string cmd(tmpl);
177 ocpn::replace(cmd, "@PATH@", get_dongle_rule());
178 auto ctrl = new wxTextCtrl(this, wxID_ANY, cmd);
179 ctrl->SetEditable(false);
180 ctrl->SetMinSize(parent->GetTextExtent(cmd + "aaa"));
181 return ctrl;
182 }
183 wxWindow* m_parent;
184};
185
187class ReviewRule : public HideShowPanel {
188public:
189 ReviewRule(wxWindow* parent, const std::string& rule)
190 : HideShowPanel(parent, 0) {
191 int from = rule[0] == '\n' ? 1 : 0;
192 m_child = new wxStaticText(this, wxID_ANY, rule.substr(from));
193 toggle();
194
195 auto flags = wxSizerFlags().Expand().Border().Right();
196 auto hbox = new wxBoxSizer(wxHORIZONTAL);
197 hbox->Add(new wxStaticText(this, wxID_ANY, _("Review rule")), flags);
198 hbox->Add(m_arrow);
199
200 auto vbox = new wxBoxSizer(wxVERTICAL);
201 vbox->Add(hbox);
202 auto indent = parent->GetTextExtent("ABCDE").GetWidth();
203 flags = flags.Border(wxLEFT, indent);
204 vbox->Add(m_child, flags.ReserveSpaceEvenIfHidden());
205 SetSizer(vbox);
206 SetAutoLayout(true);
207 Show();
208 }
209};
210
212static std::string get_rule(const std::string& path) {
213 std::ifstream input(path.c_str());
214 std::ostringstream buf;
215 buf << input.rdbuf();
216 input.close();
217 if (input.bad()) {
218 WARNING_LOG << "Cannot open rule file: " << path;
219 }
220 return buf.str();
221}
222
224class DongleInfoPanel : public wxPanel {
225public:
226 DongleInfoPanel(wxWindow* parent) : wxPanel(parent) {
227 std::string cmd(INSTRUCTIONS);
228 std::string rule_path(get_dongle_rule());
229 ocpn::replace(cmd, "@PATH@", rule_path.c_str());
230 ocpn::replace(cmd, "@pkexec@", "sudo");
231 auto vbox = new wxBoxSizer(wxVERTICAL);
232 vbox->Add(new ManualInstructions(this, cmd.c_str()));
233 std::string rule_text = get_rule(rule_path);
234 vbox->Add(new ReviewRule(this, rule_text.c_str()));
235 SetAutoLayout(true);
236 SetSizer(vbox);
237 }
238};
239
241class DeviceInfoPanel : public wxPanel {
242public:
243 DeviceInfoPanel(wxWindow* parent, const std::string rule_path)
244 : wxPanel(parent) {
245 std::string cmd(INSTRUCTIONS);
246 ocpn::replace(cmd, "@PATH@", rule_path.c_str());
247 ocpn::replace(cmd, "@pkexec@", "sudo");
248 auto vbox = new wxBoxSizer(wxVERTICAL);
249 vbox->Add(new ManualInstructions(this, cmd.c_str()));
250 vbox->Add(new ReviewRule(this, get_rule(rule_path)));
251 SetAutoLayout(true);
252 SetSizer(vbox);
253 }
254};
255
257struct Buttons : public wxPanel {
258 Buttons(wxWindow* parent, const char* rule_path)
259 : wxPanel(parent), m_rule_path(rule_path) {
260 auto sizer = new wxBoxSizer(wxHORIZONTAL);
261 auto flags = wxSizerFlags().Right().Bottom().Border();
262 sizer->Add(1, 1, 100, wxEXPAND); // Expanding spacer
263 auto install = new wxButton(this, wxID_ANY, _("Install rule"));
264 install->Bind(wxEVT_COMMAND_BUTTON_CLICKED,
265 [&](wxCommandEvent& ev) { do_install(); });
266 install->Enable(getenv("FLATPAK_ID") == NULL);
267 sizer->Add(install, flags);
268 auto quit = new wxButton(this, wxID_EXIT, _("Quit"));
269 quit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [&](wxCommandEvent& ev) {
270 if (getenv("FLATPAK_ID")) {
271 auto flags = wxOK | wxICON_INFORMATION;
272 auto msg = FLATPAK_INSTALL_MSG;
273 OCPNMessageBox(this, msg, _("OpenCPN"), flags);
274 }
275 dynamic_cast<wxDialog*>(GetParent())->EndModal(0);
276 });
277 sizer->Add(quit, flags);
278 SetSizer(sizer);
279 Fit();
280 Show();
281 }
282
283 void do_install() {
284 std::string cmd(INSTRUCTIONS);
285 ocpn::replace(cmd, "@PATH@", m_rule_path);
286 ocpn::replace(cmd, "@pkexec@", "pkexec");
287 int sts = system(cmd.c_str());
288 int flags = wxOK | wxICON_WARNING;
289 const char* msg = _("Errors encountered installing rule.");
290 if (WIFEXITED(sts) && WEXITSTATUS(sts) == 0) {
291 msg = RULE_SUCCESS_MSG;
292 flags = wxOK | wxICON_INFORMATION;
293 }
294 OCPNMessageBox(this, msg, _("OpenCPN Info"), flags);
295 }
296
297 std::string m_rule_path;
298};
299
301class DongleRuleDialog : public wxDialog {
302public:
303 DongleRuleDialog(wxWindow* parent)
304 : wxDialog(parent, wxID_ANY, _("Manage dongle udev rule")) {
305 auto sizer = new wxBoxSizer(wxVERTICAL);
306 auto flags = wxSizerFlags().Expand().Border();
307 std::string intro(DONGLE_INTRO);
308 if (getenv("FLATPAK_ID")) {
309 intro += FLATPAK_INTRO_TRAILER;
310 }
311 sizer->Add(new wxStaticText(this, wxID_ANY, intro), flags);
312 sizer->Add(new wxStaticLine(this), flags);
313 sizer->Add(new DongleInfoPanel(this), flags);
314 sizer->Add(new HidePanel(this, HIDE_DIALOG_LABEL, &hide_dongle_dialog),
315 flags.Right());
316 sizer->Add(new wxStaticLine(this), flags);
317 sizer->Add(new Buttons(this, get_dongle_rule().c_str()), flags);
318 SetSizer(sizer);
319 SetAutoLayout(true);
320 Fit();
321 }
322};
323
325static std::string get_device_intro(const char* device, std::string symlink) {
326 std::string intro(DEVICE_INTRO);
327 ocpn::replace(symlink, "/dev/", "");
328 while (intro.find("@SYMLINK@") != std::string::npos) {
329 ocpn::replace(intro, "@SYMLINK@", symlink);
330 }
331 std::string dev_name(device);
332 ocpn::replace(dev_name, "/dev/", "");
333 while (intro.find("@DEVICE@") != std::string::npos) {
334 ocpn::replace(intro, "@DEVICE@", dev_name.c_str());
335 }
336 if (getenv("FLATPAK_ID")) {
337 intro += FLATPAK_INTRO_TRAILER;
338 }
339 return intro;
340}
341
343class DeviceRuleDialog : public wxDialog {
344public:
345 DeviceRuleDialog(wxWindow* parent, const char* device_path)
346 : wxDialog(parent, wxID_ANY, _("Manage device udev rule")) {
347 auto sizer = new wxBoxSizer(wxVERTICAL);
348 auto flags = wxSizerFlags().Expand().Border();
349
350 std::string symlink(make_udev_link());
351 auto intro = get_device_intro(device_path, symlink.c_str());
352 auto rule_path = get_device_rule(device_path, symlink.c_str());
353 sizer->Add(new wxStaticText(this, wxID_ANY, intro), flags);
354 sizer->Add(new wxStaticLine(this), flags);
355 sizer->Add(new DeviceInfoPanel(this, rule_path), flags);
356 sizer->Add(new HidePanel(this, HIDE_DIALOG_LABEL, &hide_device_dialog),
357 flags.Right());
358 sizer->Add(new wxStaticLine(this), flags);
359 sizer->Add(new Buttons(this, rule_path.c_str()), flags);
360
361 SetSizer(sizer);
362 SetAutoLayout(true);
363 Fit();
364 }
365};
366
367bool CheckSerialAccess(wxWindow* parent, const std::string device) {
368 if (hide_device_dialog) {
369 return true;
370 }
371 if (!ocpn::exists(device)) {
372 std::string msg(DEVICE_NOT_FOUND);
373 ocpn::replace(msg, "@device@", device);
374 OCPNMessageBox(parent, msg, _("OpenCPN device error"));
375 return false;
376 }
377 int result = 0;
378 if (!is_device_permissions_ok(device.c_str())) {
379 auto dialog = new DeviceRuleDialog(parent, device.c_str());
380 result = dialog->ShowModal();
381 delete dialog;
382 }
383 return result == 0;
384}
385
386bool CheckDongleAccess(wxWindow* parent) {
387 int result = 0;
388 if (is_dongle_permissions_wrong() && !hide_dongle_dialog) {
389 auto dialog = new DongleRuleDialog(parent);
390 result = dialog->ShowModal();
391 delete dialog;
392 }
393 return result == 0;
394}
The device "manual instructions" and "Review rule" stuff.
Main, top-level device udev rule dialog.
The dongle "manual instructions" and "Review rule" stuff.
Main, top-level Dongle udev rule dialog.
A clickable triangle which controls child window hide/show.
Manual instructions dynamic display.
Review rule dynamic display.
Install/Quit buttons bottom-right.
The "Dont show this message next time" checkbox.
Line with "Don't show this message..." checkbox