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