1 /***************************************************************************
2 * Copyright (C) 2012-2020 by Mihai Moldovan <ionic@ionic.de> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
18 ***************************************************************************/
19
20 #include <unistd.h>
21 #include <stdlib.h>
22 #include <cerrno>
23 #include <QSysInfo>
24
25 #if QT_VERSION >= 0x050000
26 #include <QStandardPaths>
27 #endif /* QT_VERSION >= 0x050000 */
28
29 #include "pulsemanager.h"
30 #include "x2gologdebug.h"
31 #include "x2goutils.h"
32
PulseManager()33 PulseManager::PulseManager () : app_dir_ (QApplication::applicationDirPath ()),
34 pulse_X2Go_ ("/.x2go/pulse"),
35 server_binary_ (QString ("")),
36 server_working_dir_ (QString ("")),
37 server_args_ (QStringList ()),
38 pulse_server_ (NULL),
39 state_ (QProcess::NotRunning),
40 pulse_port_ (4713),
41 esd_port_ (4714),
42 pulse_version_major_ (0),
43 pulse_version_minor_ (0),
44 pulse_version_micro_ (0),
45 pulse_version_misc_ (""),
46 pulse_version_valid_ (false),
47 record_ (true),
48 playback_ (true),
49 debug_ (false),
50 system_pulse_ (false),
51 shutdown_state_ (false) {
52 pulse_dir_ = QDir (QDir::homePath ());
53 pulse_dir_.mkpath (pulse_dir_.absolutePath () + pulse_X2Go_ + "/tmp");
54 pulse_dir_.cd (pulse_X2Go_.mid (1));
55
56 env_ = QProcessEnvironment::systemEnvironment ();
57 env_.insert ("HOME", QDir::toNativeSeparators (pulse_dir_.absolutePath ()));
58 env_.insert ("TEMP", QDir::toNativeSeparators (pulse_dir_.absolutePath () + "/tmp"));
59 #ifdef Q_OS_WIN
60 env_.insert ("USERPROFILE", QDir::toNativeSeparators (pulse_dir_.absolutePath ()));
61 env_.insert ("USERNAME", "pulseuser");
62 #endif // defined (Q_OS_WIN)
63
64 /* Set server binary and working dir paths. */
65 #ifdef Q_OS_DARWIN
66 /* Assume bundled PA first. */
67 server_working_dir_ = QString (app_dir_ + "/../exe/");
68 server_binary_ = QString (server_working_dir_ + "/pulseaudio");
69
70 #if QT_VERSION < 0x050000
71 QProcessEnvironment tmp_env = QProcessEnvironment::systemEnvironment ();
72 QString path_val = tmp_env.value ("PATH");
73
74 QStringList to_front, to_back;
75 to_front << MACPORTS_PREFIX "/bin"; /* MacPorts prefix. */
76 to_back << "/usr/local/bin"; /* Homebrew or random stuff. Probably even both intermingled... */
77
78 path_val = add_to_path (path_val, to_back);
79 path_val = add_to_path (path_val, to_front, false);
80
81 server_binary_ = find_binary (server_working_dir_, "pulseaudio");
82
83 if (server_binary_.isEmpty ()) {
84 server_binary_ = find_binary (path_val, "pulseaudio");
85
86 if (!(server_binary_.isEmpty ())) {
87 system_pulse_ = true;
88 }
89 }
90 #else /* QT_VERSION < 0x050000 */
91 QStringList search_paths;
92 search_paths << server_working_dir_;
93
94 server_binary_ = QStandardPaths::findExecutable ("pulseaudio", search_paths);
95
96 if (server_binary_.isEmpty ()) {
97 search_paths = QStringList ();
98 search_paths << MACPORTS_PREFIX "/bin"; /* MacPorts prefix. */
99
100 server_binary_ = QStandardPaths::findExecutable ("pulseaudio", search_paths);
101
102 if (server_binary_.isEmpty ()) {
103 search_paths = QStringList ();
104
105 server_binary_ = QStandardPaths::findExecutable ("pulseaudio", search_paths);
106
107 if (server_binary_.isEmpty ()) {
108 search_paths = QStringList ();
109 search_paths << "/usr/local/bin"; /* Homebrew or random stuff. Probably even both intermingled... */
110
111 server_binary_ = QStandardPaths::findExecutable ("pulseaudio", search_paths);
112
113 if (!(server_binary_.isEmpty ())) {
114 system_pulse_ = true;
115 }
116 }
117 else {
118 system_pulse_ = true;
119 }
120 }
121 else {
122 system_pulse_ = true;
123 }
124 }
125 #endif /* QT_VERSION < 0x050000 */
126
127 if (!(server_binary_.isEmpty ())) {
128 QFileInfo tmp_file_info = QFileInfo (server_binary_);
129 server_working_dir_ = tmp_file_info.canonicalPath ();
130
131 x2goDebug << "Found PA binary as " << server_binary_;
132 x2goDebug << "Corresponding working dir: " << server_working_dir_;
133 }
134
135 #elif defined (Q_OS_WIN)
136 server_working_dir_ = QString (app_dir_ + "/pulse/");
137 server_binary_ = QString (app_dir_ + "/pulse/pulseaudio.exe");
138 #elif defined (Q_OS_UNIX)
139 std::ssize_t path_len = pathconf (".", _PC_PATH_MAX);
140
141 if (-1 == path_len) {
142 path_len = 1024;
143 }
144
145 char *buf, *ptr;
146
147 for (buf = ptr = NULL; ptr == NULL; path_len += 20) {
148 if (NULL == (buf = static_cast<char *> (realloc (buf, path_len)))) {
149 x2goErrorf (16) << "Could not allocate buffer for getting current working directory!";
150 emit sig_pulse_user_warning (true, tr ("Could not allocate buffer for getting current working directory!"),
151 QString (),
152 true);
153 abort ();
154 }
155
156 memset (buf, 0, path_len);
157 ptr = getcwd (buf, path_len);
158
159 if ((NULL == ptr) && (ERANGE != errno)) {
160 int saved_errno = errno;
161 x2goErrorf (17) << "getcwd() failed: " << QString (strerror (saved_errno));
162 emit sig_pulse_user_warning (true, tr ("getcwd() failed!"),
163 QString (strerror (saved_errno)),
164 true);
165 abort ();
166 }
167 }
168
169 server_working_dir_ = QString (buf);
170 server_binary_ = QString ("pulseaudio");
171
172 free (buf);
173 buf = ptr = NULL;
174 #endif // defined (Q_OS_DARWIN)
175
176 if (!(server_binary_.isEmpty ())) {
177 fetch_pulseaudio_version ();
178 }
179 }
180
~PulseManager()181 PulseManager::~PulseManager () {
182 if (pulse_server_ && is_server_running ())
183 shutdown ();
184
185 delete (pulse_server_);
186 }
187
start()188 void PulseManager::start () {
189 if (is_server_running ()) {
190 return;
191 }
192
193 delete (pulse_server_);
194
195 pulse_server_ = new QProcess (0);
196 state_ = QProcess::Starting;
197
198 // Search for a free Pulse and EsounD port.
199 // Note that there is no way we could find
200 // an esd port, if the pulse port detection
201 // failed. Better trust your compiler to
202 // optimize this statement and save some
203 // cycles.
204 if ((find_port (false)) && (find_port (true))) {
205 #ifdef Q_OS_DARWIN
206 start_osx ();
207 #elif defined (Q_OS_WIN)
208 start_win ();
209 #elif defined (Q_OS_UNIX)
210 start_linux ();
211 #endif // defined (Q_OS_DARWIN)
212 }
213 }
214
start_generic()215 void PulseManager::start_generic () {
216 pulse_server_->setProcessEnvironment (env_);
217 pulse_server_->setWorkingDirectory (server_working_dir_);
218
219 if (!(server_binary_.isEmpty ())) {
220 pulse_server_->start (server_binary_, server_args_);
221
222 /*
223 * We may wait here, because PulseManager runs in a separate thread.
224 * Otherwise, we'd better use signals and slots to not block the main thread.
225 */
226 if (pulse_server_->waitForStarted (-1)) {
227 x2goDebug << "pulse started with arguments " << server_args_ << "- waiting for it to finish...";
228 state_ = QProcess::Running;
229
230 connect (pulse_server_, SIGNAL (finished (int)),
231 this, SLOT (slot_on_pulse_finished (int)));
232
233 env_.insert ("PULSE_SERVER", "127.0.0.1:" + QString::number (pulse_port_));
234
235
236 QString clean_pulse_dir = pulse_dir_.absolutePath ();
237
238 #ifdef Q_OS_WIN
239 clean_pulse_dir = wapiShortFileName (clean_pulse_dir);
240 #endif /* defined (Q_OS_WIN) */
241
242 QString tmp_auth_cookie = QDir::toNativeSeparators (clean_pulse_dir + "/.pulse-cookie");
243
244 env_.insert ("PULSE_COOKIE", tmp_auth_cookie);
245
246 if (debug_) {
247 // Give PA a little time to come up.
248 QTimer::singleShot (3000, this, SLOT (slot_play_startup_sound ()));
249 }
250 }
251 else {
252 x2goErrorf (27) << "PulseAudio failed to start! Sound support will not be available.";
253 show_startup_warning ();
254 }
255 }
256 else {
257 x2goErrorf (31) << "PulseAudio not detected on system! Sound support will not be available.";
258 show_startup_warning ();
259 }
260 }
261
start_osx()262 void PulseManager::start_osx () {
263 server_args_ = QStringList ();
264 server_args_ << "--exit-idle-time=-1" << "-n"
265 << "-F" << QDir::toNativeSeparators (pulse_dir_.absolutePath () + "/config.pa");
266
267 if (!system_pulse_) {
268 if (!(pulse_version_valid_)) {
269 x2goDebug << "PulseAudio version number was not fetched successfully, data is invalid. Continuing anyway.";
270 }
271
272 server_args_ << "-p"
273 << QDir::toNativeSeparators (QDir (app_dir_
274 + "/../Frameworks/pulse-"
275 + QString::number (pulse_version_major_)
276 + "."
277 + QString::number (pulse_version_minor_)
278 + "/modules").absolutePath ());
279 }
280
281 server_args_ << "--high-priority";
282
283 if (debug_) {
284 server_args_ << "--log-level=debug"
285 << "--verbose"
286 << "--log-target=file:" + QDir::toNativeSeparators (pulse_dir_.absolutePath () + "/pulse.log");
287 }
288
289 if (generate_server_config () && generate_client_config ()) {
290 cleanup_client_dir ();
291
292 start_generic ();
293 }
294 }
295
start_win()296 void PulseManager::start_win () {
297 /*
298 * Some code in here is Windows-specific and will lead to compile
299 * failures on other platforms. Make this a stub for everything non-Windows.
300 */
301 #ifdef Q_OS_WIN
302 server_args_ = QStringList ();
303
304 if (!(pulse_version_valid_)) {
305 x2goDebug << "PulseAudio version number was not fetched successfully, data is invalid. Continuing anyway.";
306 }
307
308 server_args_ << "--exit-idle-time=-1" << "-n"
309 << "-F" << QDir::toNativeSeparators (QDir (pulse_dir_.absolutePath ()
310 + "/config.pa").absolutePath ())
311 << "-p" << QDir::toNativeSeparators (QDir (app_dir_ + "/pulse/lib/pulse-"
312 + QString::number (pulse_version_major_)
313 + "."
314 + QString::number (pulse_version_minor_)
315 + "/modules/").absolutePath ());
316 if (debug_) {
317 server_args_ << "--log-level=debug"
318 << "--verbose"
319 << "--log-target=file:" + QDir::toNativeSeparators (pulse_dir_.absolutePath () + "/pulse.log");
320 }
321
322 /*
323 * Fix for x2goclient bug #526.
324 * Works around PulseAudio bug #80772.
325 * Tested with PulseAudio 5.0.
326 * This argument will not cause PulseAudio 0.9.6 or 1.1 (the legacy versions)
327 * to fail to launch.
328 * However, 0.9.6 defaults to normal priority anyway,
329 * and 1.1 ignores it for some reason.
330 * So yes, the fact that 1.1 ignores it would be a bug in x2goclient if we
331 * ever ship 1.1 again.
332 */
333 if ((QSysInfo::WindowsVersion == QSysInfo::WV_XP) || (QSysInfo::WindowsVersion == QSysInfo::WV_2003)) {
334 x2goDebug << "Windows XP or Server 2003 (R2) detected."
335 << "Setting PulseAudio to \"normal\" CPU priority.";
336
337 server_args_ << "--high-priority=no";
338 }
339
340 if (generate_server_config () && generate_client_config ()) {
341 create_client_dir ();
342
343 start_generic ();
344 }
345 #endif /* defined (Q_OS_WIN) */
346 }
347
start_linux()348 void PulseManager::start_linux () {
349 /* Do nothing - assumed to be already running. */
350 }
351
fetch_pulseaudio_version()352 void PulseManager::fetch_pulseaudio_version () {
353 QStringList args (QString ("--version"));
354 QProcess tmp_server (this);
355
356 /* Start PA with --version argument. */
357 tmp_server.setWorkingDirectory (server_working_dir_);
358 tmp_server.start (server_binary_, args);
359
360 bool stop_processing = false;
361
362 /* Wait until the process exited again. */
363 if (tmp_server.waitForFinished ()) {
364 /* Read stdout and split it up on newlines. */
365 QByteArray ba (tmp_server.readAllStandardOutput ());
366 QString stdout_data (ba.constData ());
367 QStringList stdout_list (stdout_data.split ("\n"));
368
369 x2goDebug << "pulseaudio --version returned:" << stdout_data << endl;
370
371 bool found = false;
372 for (QStringList::const_iterator cit = stdout_list.begin (); (cit != stdout_list.end ()) && (!stop_processing); ++cit) {
373 /* Remove trailing whitespace, mostly carriage returns on Windows. */
374 QString tmp_str (*cit);
375 tmp_str = tmp_str.trimmed ();
376
377 QString needle ("pulseaudio ");
378
379 if (tmp_str.startsWith (needle)) {
380 /* Drop first part. */
381 tmp_str = tmp_str.mid (needle.size ());
382
383 /* We should be at a digit now. */
384 bool numbers_started[3] = { false, false, false };
385 bool numbers_finished[3] = { false, false, false };
386 bool numbers_skip[3] = { false, false, false };
387 QString tmp_remaining_str = QString ("");
388 QString numbers[3] = { };
389 for (QString::const_iterator cit = tmp_str.begin (); (cit != tmp_str.end ()) && (!stop_processing); ++cit) {
390 if (!(numbers_finished[0])) {
391 if (((*cit) >= '0') && ((*cit) <= '9')) {
392 numbers[0].append (*cit);
393 numbers_started[0] = true;
394 }
395 else if ((*cit) == '.') {
396 /* First number part complete and more to come, mark as done. */
397 numbers_finished[0] = true;
398 }
399 else if ((*cit) == '-') {
400 /* First number part complete and no more numbers, mark as done, and... */
401 numbers_finished[0] = true;
402
403 /*
404 * Skip all the other numbers (i.e., assume the default value.)
405 * This doesn't make a huge lot of sense for the first number,
406 * but let's make this robust...
407 */
408 numbers_skip[1] = true;
409 numbers_skip[2] = true;
410 }
411 else {
412 x2goErrorf (21) << "Unexpected character found when parsing version string for major version number: '" << QString (*cit) << "'.";
413 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
414 tr ("Unexpected character found when parsing version string for major version number")
415 + ": '" + QString (*cit) + "'.",
416 true);
417 stop_processing = true;
418 }
419 }
420 else if ((!(numbers_finished[1])) && (!(numbers_skip[1]))) {
421 if (((*cit) >= '0') && ((*cit) <= '9')) {
422 numbers[1].append (*cit);
423 numbers_started[1] = true;
424 }
425 else if ((*cit) == '.') {
426 /* Second number part complete and more to come, mark as done. */
427 numbers_finished[1] = true;
428 }
429 else if ((*cit) == '-') {
430 /* Second number part complete and no more numbers, mark as done, and... */
431 numbers_finished[1] = true;
432
433 /* Skip all the other numbers (i.e., assume the default value.) */
434 numbers_skip[2] = true;
435 }
436 else {
437 x2goErrorf (23) << "Unexpected character found when parsing version string for minor version number: '" << QString (*cit) << "'.";
438 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
439 tr ("Unexpected character found when parsing version string for minor version number")
440 + ": '" + QString (*cit) + "'.",
441 true);
442 stop_processing = true;
443 }
444 }
445 else if ((!(numbers_finished[2])) && (!(numbers_skip[2]))) {
446 if (((*cit) >= '0') && ((*cit) <= '9')) {
447 numbers[2].append (*cit);
448 numbers_started[2] = true;
449 }
450 else if ((*cit) == '-') {
451 /* Third number part complete and no more numbers, mark as done. */
452 numbers_finished[2] = true;
453 }
454 else {
455 x2goErrorf (25) << "Unexpected character found when parsing version string for micro version number: '" << QString (*cit) << "'.";
456 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
457 tr ("Unexpected character found when parsing version string for micro version number")
458 + ": '" + QString (*cit) + "'.",
459 true);
460 stop_processing = true;
461 }
462 }
463 else {
464 /* Numbers should be good by now, let's fetch everything else. */
465 tmp_remaining_str.append (*cit);
466 }
467 }
468
469 if (numbers_skip[0]) {
470 x2goErrorf (30) << "Supposed to skip major version number. Something is wrong.";
471 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
472 tr ("Supposed to skip major version number. "
473 "Something is wrong."),
474 true);
475 stop_processing = true;
476 }
477
478 /* not skipping and ((met period or dash) or (have something to convert and met EOL)) */
479 if ((!numbers_skip[0]) && ((numbers_finished[0]) || (numbers_started[0]))) {
480 bool convert_success = false;
481 pulse_version_major_ = numbers[0].toUInt (&convert_success, 10);
482
483 if (!convert_success) {
484 x2goErrorf (20) << "Unable to convert major version number string to integer.";
485 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
486 tr ("Unable to convert major version number string to integer."),
487 true);
488 stop_processing = true;
489 }
490 else {
491 /* First number is enough to satisfy the "found" criterion. */
492 found = true;
493 }
494 }
495
496 /* not skipping and ((met period or dash) or (have something to convert and met EOL)) */
497 if ((!numbers_skip[1]) && ((numbers_finished[1]) || (numbers_started[1]))) {
498 bool convert_success = false;
499 pulse_version_minor_ = numbers[1].toUInt (&convert_success, 10);
500
501 if (!convert_success) {
502 x2goErrorf (22) << "Unable to convert minor version number string to integer.";
503 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
504 tr ("Unable to convert minor version number string to integer."),
505 true);
506 stop_processing = true;
507 }
508 }
509
510 /* not skipping and ((met period or dash) or (have something to convert and met EOL)) */
511 if ((!numbers_skip[2]) && ((numbers_finished[2]) || (numbers_started[2]))) {
512 bool convert_success = false;
513 pulse_version_micro_ = numbers[2].toUInt (&convert_success, 10);
514
515 if (!convert_success) {
516 x2goErrorf (24) << "Unable to convert micro version number string to integer.";
517 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
518 tr ("Unable to convert micro version number string to integer."),
519 true);
520 stop_processing = true;
521 }
522 }
523
524 /* Misc version part will be set to the trailing string. */
525 if (found) {
526 pulse_version_misc_ = tmp_remaining_str;
527
528 pulse_version_valid_ = true;
529
530 stop_processing = true;
531 break;
532 }
533 }
534 else {
535 /* No need to look any further. */
536 continue;
537 }
538 }
539
540 if (!found) {
541 x2goErrorf (19) << "Unable to fetch PulseAudio version - unexpected format.";
542 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
543 tr ("Unexpected format encountered."),
544 true);
545 }
546 }
547 else {
548 x2goErrorf (18) << "Unable to start PulseAudio to fetch its version number.";
549 emit sig_pulse_user_warning (true, tr ("Error fetching PulseAudio version number!"),
550 tr ("Unable to start PulseAudio binary."),
551 true);
552 }
553 }
554
find_port(bool search_esd)555 bool PulseManager::find_port (bool search_esd) {
556 QTcpSocket tcpSocket (0);
557 bool free = false;
558 std::uint16_t search_port = pulse_port_;
559 std::uint16_t other_port = esd_port_;
560
561 // If the search_esd parameter is true, find a free port
562 // for the PulseAudio emulation.
563 if (search_esd) {
564 search_port = esd_port_;
565 other_port = pulse_port_;
566 }
567
568 do {
569 // Skip this port, if it's reserved for the counterpart.
570 if (search_port == other_port) {
571 ++search_port;
572 continue;
573 }
574
575 tcpSocket.connectToHost ("127.0.0.1", search_port);
576
577 if (tcpSocket.waitForConnected (1000)) {
578 tcpSocket.close ();
579 free = false;
580 ++search_port;
581 }
582 else {
583 free = true;
584 }
585 } while ((!free) && (search_port > 1023));
586
587 if (!search_esd) {
588 pulse_port_ = search_port;
589 }
590 else {
591 esd_port_ = search_port;
592 }
593
594 return (free);
595 }
596
generate_server_config()597 bool PulseManager::generate_server_config () {
598 QString config_file_name (pulse_dir_.absolutePath () + "/config.pa");
599 QTemporaryFile config_tmp_file (pulse_dir_.absolutePath () + "/tmp/tmpconfig");
600 bool ret = false;
601
602 if (config_tmp_file.open ()) {
603 QTextStream config_tmp_file_stream (&config_tmp_file);
604
605 config_tmp_file_stream << "load-module module-native-protocol-tcp port="
606 + QString::number (pulse_port_);
607
608 /*
609 * Reference:
610 * http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/#index22h3
611 *
612 * Setting auth-cookie fixes bug #422.
613 *
614 * PulseAudio 6.0 changed the path that auth-cookie is relative to, so
615 * Tanu Kaskinen recommended we specify the absolute path instead.
616 * The absolute path works with at least 5.0 and 6.0.
617 */
618 if ((!(pulse_version_valid_)) || (pulse_version_major_ > 2)) {
619 QString clean_pulse_dir = pulse_dir_.absolutePath ();
620
621 #ifdef Q_OS_WIN
622 clean_pulse_dir = wapiShortFileName (clean_pulse_dir);
623 #endif /* defined (Q_OS_WIN) */
624
625 QString tmp_auth_cookie = QDir::toNativeSeparators (clean_pulse_dir + "/.pulse-cookie");
626
627 #ifdef Q_OS_WIN
628 /* Double backslashes are required in config.pa. */
629 tmp_auth_cookie.replace ("\\", "\\\\");
630 #endif /* defined (Q_OS_WIN) */
631
632 config_tmp_file_stream << " auth-cookie=" + tmp_auth_cookie;
633 }
634
635 config_tmp_file_stream << endl;
636
637 #ifdef Q_OS_UNIX
638 config_tmp_file_stream << "load-module module-native-protocol-unix" << endl;
639 config_tmp_file_stream << "load-module module-esound-protocol-unix" << endl;
640 #endif // defined(Q_OS_UNIX)
641
642 config_tmp_file_stream << "load-module module-esound-protocol-tcp port="
643 << QString::number (esd_port_)
644 << endl;
645
646 #ifdef Q_OS_DARWIN
647 config_tmp_file_stream << "load-module module-coreaudio-detect";
648 #elif defined (Q_OS_WIN)
649 config_tmp_file_stream << "load-module module-waveout";
650 // FIXME Linux
651 #endif // defined (Q_OS_DARWIN)
652
653 config_tmp_file_stream << " record=";
654 if (!record_) {
655 config_tmp_file_stream << "0";
656 }
657 else {
658 config_tmp_file_stream << "1";
659 }
660
661 config_tmp_file_stream << " playback=";
662 if (!playback_) {
663 config_tmp_file_stream << "0";
664 }
665 else {
666 config_tmp_file_stream << "1";
667 }
668 config_tmp_file_stream << endl;
669
670 QFile config_file (config_file_name);
671 if (QFile::exists (config_file_name))
672 QFile::remove (config_file_name);
673
674 config_tmp_file.copy (config_file_name);
675 config_tmp_file.remove ();
676
677 ret = true;
678 }
679
680 return (ret);
681 }
682
generate_client_config()683 bool PulseManager::generate_client_config () {
684 QTemporaryFile client_config_tmp_file (pulse_dir_.absolutePath ()
685 + "/tmp/tmpconfig");
686 QString client_config_file_name (pulse_dir_.absolutePath ()
687 + "/.pulse/client.conf");
688 bool ret = false;
689
690 if (client_config_tmp_file.open ()) {
691 QTextStream config_tmp_file_stream (&client_config_tmp_file);
692
693 config_tmp_file_stream << "autospawn=no" << endl;
694 #ifdef Q_OS_WIN
695 config_tmp_file_stream << "default-server=localhost:" << pulse_port_ << endl;
696 #endif // defined (Q_OS_WIN)
697 config_tmp_file_stream << "daemon-binary="
698 << QDir::toNativeSeparators (QDir (server_binary_).absolutePath ())
699 << endl;
700
701 if (QFile::exists (client_config_file_name))
702 QFile::remove (client_config_file_name);
703
704 QDir client_config_dir (pulse_dir_.absolutePath () + "/.pulse/");
705 client_config_dir.mkpath (client_config_dir.absolutePath ());
706
707 client_config_tmp_file.copy (client_config_file_name);
708 client_config_tmp_file.remove ();
709
710 ret = true;
711 }
712
713 return (ret);
714 }
715
cleanup_client_dir()716 void PulseManager::cleanup_client_dir () {
717 // PA expects $HOME/.pulse/$HOST-runtime to be a symbolic link
718 // and will fail, if it's just a plain directory on Mac OS X.
719 // Delete it first.
720 QDir machine_dir (pulse_dir_.absolutePath () + "/.pulse/"
721 + QHostInfo::localHostName () + "-runtime");
722
723 if (QFile::exists (machine_dir.absolutePath () + "/pid"))
724 QFile::remove (machine_dir.absolutePath () + "/pid");
725
726 if (machine_dir.exists ())
727 machine_dir.remove (machine_dir.absolutePath ());
728 }
729
create_client_dir()730 void PulseManager::create_client_dir () {
731 QDir machine_dir (pulse_dir_.absolutePath () + "/.pulse/"
732 + QHostInfo::localHostName () + "-runtime");
733
734 if (!machine_dir.exists ())
735 machine_dir.mkpath (machine_dir.absolutePath ());
736
737 if (QFile::exists (machine_dir.absolutePath () + "/pid"))
738 QFile::remove (machine_dir.absolutePath () + "/pid");
739 }
740
slot_play_startup_sound()741 void PulseManager::slot_play_startup_sound () {
742 if (debug_) {
743 QProcess play_file (0);
744
745 /*
746 * Assume paplay is located at the same place as
747 * the pulseaudio binary.
748 */
749 QString play_file_binary (server_working_dir_);
750
751 QString play_file_file (app_dir_);
752
753 #ifdef Q_OS_DARWIN
754 play_file_binary += "/paplay";
755 play_file_file += "/../Resources/startup.wav";
756 #elif defined (Q_OS_WIN)
757 play_file_binary += "/paplay.exe";
758 play_file_file += "/startup.wav";
759 #else
760 /* FIXME: implement Linux section. */
761 #endif // defined (Q_OS_DARWIN)
762
763 QStringList args (play_file_file);
764 play_file.setWorkingDirectory (server_working_dir_);
765 play_file.setProcessEnvironment (env_);
766 play_file.start (play_file_binary, args);
767
768 if (play_file.waitForStarted (-1)) {
769 play_file.waitForFinished ();
770 }
771 else {
772 x2goErrorf (26) << "Unable to play startup sound! Something may be wrong.";
773 show_startup_warning (true);
774 }
775 }
776 }
777
slot_on_pulse_finished(int exit_code)778 void PulseManager::slot_on_pulse_finished (int exit_code) {
779 if (exit_code && !shutdown_state_)
780 {
781 x2goDebug << "Warning! Pulseaudio's exit code is non-zero.";
782 show_startup_warning(true);
783 }
784 shutdown_state_ = false;
785 x2goDebug << "Pulseaudio finished with code:"<<exit_code;
786 QByteArray ba (pulse_server_->readAllStandardOutput ());
787 const char *data = ba.constData ();
788 x2goDebug << data;
789 ba = pulse_server_->readAllStandardError ();
790 data = ba.constData ();
791 x2goDebug << data;
792
793 // Clean up
794 QDir work_dir (app_dir_);
795
796 #ifdef Q_OS_WIN
797 work_dir.remove (pulse_dir_.absolutePath ()
798 + "/pulse-pulseuser/pid");
799 work_dir.rmdir (pulse_dir_.absolutePath ()
800 + "/pulse-pulseuser");
801 #endif // defined (Q_OS_WIN)
802
803 #if defined (Q_OS_DARWIN) || defined (Q_OS_WIN)
804 // Remove server config file, otherwise the directory won't be empty.
805 if (!debug_) {
806 work_dir.remove (QDir::toNativeSeparators (QDir (pulse_dir_.absolutePath ()
807 + "/config.pa").absolutePath ()));
808 work_dir.remove (QDir::toNativeSeparators (QDir (pulse_dir_.absolutePath ()
809 + "/pulse.log").absolutePath ()));
810 }
811 #else // Linux
812 // FIXME.
813 #endif
814
815 work_dir.rmdir (pulse_dir_.absolutePath ());
816
817 state_ = QProcess::NotRunning;
818 emit (sig_pulse_server_terminated ());
819 }
820
is_server_running() const821 bool PulseManager::is_server_running () const {
822 if (pulse_server_)
823 return (pulse_server_->state () == QProcess::Running);
824 else
825 return (false);
826 }
827
shutdown()828 void PulseManager::shutdown () {
829 QEventLoop loop;
830
831 shutdown_state_ = true;
832 connect (this, SIGNAL (sig_pulse_server_terminated ()),
833 &loop, SLOT (quit ()));
834
835 // Console applications without an event loop can only be terminated
836 // by QProcess::kill() on Windows (unless they handle WM_CLOSE, which
837 // PA obviously doesn't.)
838 #ifdef Q_OS_WIN
839 pulse_server_->kill ();
840 #else // defined (Q_OS_WIN)
841 pulse_server_->terminate ();
842 #endif // defined (Q_OS_WIN)
843
844 loop.exec ();
845 }
846
get_pulse_port() const847 std::uint16_t PulseManager::get_pulse_port () const {
848 return (pulse_port_);
849 }
850
get_esd_port() const851 std::uint16_t PulseManager::get_esd_port () const {
852 return (esd_port_);
853 }
854
get_record() const855 bool PulseManager::get_record () const {
856 return (record_);
857 }
858
get_playback() const859 bool PulseManager::get_playback () const {
860 return (playback_);
861 }
862
get_pulse_dir() const863 QDir PulseManager::get_pulse_dir () const {
864 return (pulse_dir_);
865 }
866
set_pulse_port(std::uint16_t pulse_port)867 bool PulseManager::set_pulse_port (std::uint16_t pulse_port) {
868 bool ret = false;
869
870 if (!(is_server_running ())) {
871 pulse_port_ = pulse_port;
872 ret = true;
873 }
874
875 return (ret);
876 }
877
set_esd_port(std::uint16_t esd_port)878 bool PulseManager::set_esd_port (std::uint16_t esd_port) {
879 bool ret = false;
880
881 if (!(is_server_running ())) {
882 esd_port_ = esd_port;
883 ret = true;
884 }
885
886 return (ret);
887 }
888
set_record(bool record)889 bool PulseManager::set_record (bool record) {
890 bool ret = false;
891
892 if (!(is_server_running ())) {
893 ret = true;
894 }
895 record_ = record;
896 return (ret);
897 }
898
set_playback(bool playback)899 bool PulseManager::set_playback (bool playback) {
900 bool ret = false;
901
902 if (!(is_server_running ())) {
903 playback_ = playback;
904 ret = true;
905 }
906
907 return (ret);
908 }
909
set_debug(bool debug)910 void PulseManager::set_debug (bool debug) {
911 debug_ = debug;
912 }
913
restart()914 void PulseManager::restart () {
915 if (pulse_server_ && is_server_running ())
916 shutdown ();
917
918 x2goDebug << "restarting pulse";
919 start ();
920 }
921
state()922 QProcess::ProcessState PulseManager::state () {
923 return (state_);
924 }
925
show_startup_warning(bool play_startup_sound)926 void PulseManager::show_startup_warning (bool play_startup_sound){
927 QString main_text, informative_text;
928
929 if (!(play_startup_sound)) {
930 main_text = tr ("Unable to play startup sound.");
931 }
932 else {
933 main_text = tr ("PulseAudio failed to start!");
934 informative_text = tr ("Sound support will not be available.") + "\n\n";
935 }
936
937 informative_text += tr ("If you downloaded the bundled, pre-compiled version from the official home page "
938 "or the upstream Linux packages, please report a bug on:\n"
939 "<center><a href=\"https://wiki.x2go.org/doku.php/wiki:bugs\">"
940 "https://wiki.x2go.org/doku.php/wiki:bugs"
941 "</a></center>\n");
942
943 emit sig_pulse_user_warning (false, main_text, informative_text, true);
944 }
945