DebugServer2
Loading...
Searching...
No Matches
ProcessLaunchMixin.hpp
1//
2// Copyright (c) 2014-present, Facebook, Inc.
3// All rights reserved.
4//
5// This source code is licensed under the University of Illinois/NCSA Open
6// Source License found in the LICENSE file in the root directory of this
7// source tree. An additional grant of patent rights can be found in the
8// PATENTS file in the same directory.
9//
10
11#pragma once
12
13#include "DebugServer2/GDBRemote/Mixins/ProcessLaunchMixin.h"
14
15#include <algorithm>
16#include <filesystem>
17
18namespace ds2 {
19namespace GDBRemote {
20
21template <typename T>
22ErrorCode ProcessLaunchMixin<T>::onDisableASLR(Session &session, bool disable) {
23 _disableASLR = disable;
24 return kSuccess;
25}
26
27template <typename T>
28ErrorCode
29ProcessLaunchMixin<T>::onSetArchitecture(Session &,
30 std::string const &architecture) {
31 // Ignored for now.
32 return kSuccess;
33}
34
35template <typename T>
36ErrorCode
37ProcessLaunchMixin<T>::onSetWorkingDirectory(Session &,
38 std::string const &path) {
39 // ds2 serves each platform client on its own thread with its own session
40 // (see PlatformMain in Sources/main.cpp), so the working directory has to
41 // stay session-local state rather than ds2's actual process cwd, which is
42 // shared by every concurrently-connected client.
43 //
44 // Canonicalize to an absolute path here, once, rather than storing
45 // whatever the client sent verbatim: a relative path only means
46 // something relative to ds2's own starting directory (there is nothing
47 // else for it to be relative to, since ds2's cwd never changes), and
48 // resolving it eagerly means every later consumer of _workingDirectory
49 // (vFile path resolution, ProcessSpawner) can treat it as absolute
50 // without redoing this resolution or applying it a second time.
51 std::filesystem::path resolved(path);
52 if (!resolved.is_absolute())
53 resolved = std::filesystem::path(Platform::GetWorkingDirectory()) / resolved;
54
55 _workingDirectory = resolved.string();
56 return kSuccess;
57}
58
59template <typename T>
60ErrorCode
61ProcessLaunchMixin<T>::onQueryWorkingDirectory(Session &,
62 std::string &workingDir) const {
63 workingDir = _workingDirectory;
64 return kSuccess;
65}
66
67template <typename T>
68ErrorCode ProcessLaunchMixin<T>::onSetEnvironmentVariable(Session &,
69 std::string const &name, std::string const &value) {
70 if (value.empty()) {
71 _environment.erase(name);
72 } else {
73 _environment.insert(std::make_pair(name, value));
74 }
75 return kSuccess;
76}
77
78template <typename T>
79ErrorCode ProcessLaunchMixin<T>::onSetStdFile(Session &, int fileno,
80 std::string const &path) {
81 DS2LOG(Debug, "stdfile[%d] = %s", fileno, path.c_str());
82
83 if (fileno < 0 || fileno > 2)
84 return kErrorInvalidArgument;
85
86 if (fileno == 0) {
87 //
88 // TODO it seems that QSetSTDIN is the first message sent when
89 // launching a new process, it may be sane to invalidate all
90 // the process state at this point.
91 //
92 _disableASLR = false;
93 _arguments.clear();
94 _environment.clear();
95 _workingDirectory.clear();
96 _stdFile[0].clear();
97 _stdFile[1].clear();
98 _stdFile[2].clear();
99 }
100
101 _stdFile[fileno] = path;
102 return kSuccess;
103}
104
105template <typename T>
106ErrorCode
107ProcessLaunchMixin<T>::onSetProgramArguments(Session &,
108 StringCollection const &args) {
109 _arguments = args;
110 if (isDebugServer(args)) {
111 // Propagate current logging settings when launching an instance of
112 // the debug server to match qLaunchGDBServer behavior.
113 if (GetLogLevel() == kLogLevelDebug)
114 _arguments.push_back("--debug");
115 else if (GetLogLevel() == kLogLevelPacket)
116 _arguments.push_back("--remote-debug");
117 }
118
119 return spawnProcess();
120
121}
122
123template <typename T>
124ErrorCode ProcessLaunchMixin<T>::onQueryLaunchSuccess(Session &,
125 ProcessId) const {
126 return _lastLaunchResult;
127}
128
129template <typename T>
130ErrorCode
131ProcessLaunchMixin<T>::onQueryProcessInfo(Session &,
132 ProcessInfo &info) const {
133 if (_processList.empty())
134 return kErrorProcessNotFound;
135
136 const ProcessId pid = _processList.back();
137 if (!Platform::GetProcessInfo(pid, info))
138 return kErrorUnknown;
139
140 return kSuccess;
141}
142
143template <typename T>
144ErrorCode ProcessLaunchMixin<T>::onTerminate(Session &session,
145 ProcessThreadId const &ptid,
146 StopInfo &stop) {
147 auto it = std::find(_processList.begin(), _processList.end(), ptid.pid);
148 if (it == _processList.end())
149 return kErrorNotFound;
150
151 if (!Platform::TerminateProcess(ptid.pid))
152 return kErrorUnknown;
153
154 DS2LOG(Debug, "killed spawned process %" PRI_PID, *it);
155 _processList.erase(it);
156
157 // StopInfo is not populated by this implemenation of onTerminate since it is
158 // only ever called in the context of a platform session in response to a
159 // qKillSpawnedProcess packet.
160 stop.clear();
161 return kSuccess;
162}
163
164// Some test cases use this code path to launch additional instances of the
165// debug server rather than sending a qLaunchGDBServer packet. Allow detection
166// of this scenario so we can propagate logging settings and make debugging
167// test failures easier.
168template <typename T>
169bool ProcessLaunchMixin<T>::isDebugServer(StringCollection const &args) {
170 return (args.size() > 1)
171 && (args[0] == Platform::GetSelfExecutablePath())
172 && (args[1] == "gdbserver");
173}
174
175template <typename T>
176ErrorCode ProcessLaunchMixin<T>::spawnProcess() {
177 const bool displayArgs = _arguments.size() > 1;
178 const bool displayEnv = !_environment.empty();
179 auto it = _arguments.begin();
180 DS2LOG(Debug, "spawning process '%s'%s", (it++)->c_str(),
181 displayArgs ? " with args:" : "");
182 while (it != _arguments.end()) {
183 DS2LOG(Debug, " %s", (it++)->c_str());
184 }
185
186 if (displayEnv) {
187 DS2LOG(Debug, "%swith environment:", displayArgs ? "and " : "");
188 for (auto const &val : _environment) {
189 DS2LOG(Debug, " %s=%s", val.first.c_str(), val.second.c_str());
190 }
191 }
192
193 if (!_workingDirectory.empty()) {
194 DS2LOG(Debug, "%swith working directory: %s",
195 displayArgs || displayEnv ? "and " : "",
196 _workingDirectory.c_str());
197 }
198
199 Host::ProcessSpawner ps;
200 if (!ps.setExecutable(_arguments[0]) ||
201 !ps.setArguments(StringCollection(_arguments.begin() + 1,
202 _arguments.end())) ||
203 !ps.setWorkingDirectory(_workingDirectory) ||
204 !ps.setEnvironment(_environment))
205 return kErrorInvalidArgument;
206
207 if (isDebugServer(_arguments)) {
208 // Always log to the console when launching an instance of the debug server
209 // to match qLaunchGDBServer behavior.
210 ps.redirectInputToNull();
211 ps.redirectOutputToConsole();
212 ps.redirectErrorToConsole();
213 } else {
214 if (!_stdFile[0].empty() && !ps.redirectInputToFile(_stdFile[0]))
215 return kErrorInvalidArgument;
216 if (!_stdFile[1].empty() && !ps.redirectOutputToFile(_stdFile[1]))
217 return kErrorInvalidArgument;
218 if (!_stdFile[2].empty() && !ps.redirectErrorToFile(_stdFile[2]))
219 return kErrorInvalidArgument;
220 }
221
222 _lastLaunchResult = ps.run();
223 if (_lastLaunchResult != kSuccess)
224 return _lastLaunchResult;
225
226 // Add the pid to the list of launched processes. It will be removed when
227 // onTerminate is called.
228 _processList.push_back(ps.pid());
229
230 DS2LOG(Debug, "launched %s as process %" PRI_PID, _arguments[0].c_str(),
231 ps.pid());
232
233 return kSuccess;
234}
235} // namespace GDBRemote
236} // namespace ds2