1 /*
2 * SessionPackageProvidedExtension.cpp
3 *
4 * Copyright (C) 2021 by RStudio, PBC
5 *
6 * Unless you have received this program directly from RStudio pursuant
7 * to the terms of a commercial license agreement with RStudio, then
8 * this program is licensed to you under the terms of version 3 of the
9 * GNU Affero General Public License. This program is distributed WITHOUT
10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13 *
14 */
15
16 #include <session/SessionPackageProvidedExtension.hpp>
17
18 #include <boost/regex.hpp>
19 #include <boost/date_time/posix_time/posix_time.hpp>
20
21 #include <core/Algorithm.hpp>
22 #include <core/Exec.hpp>
23 #include <core/FileSerializer.hpp>
24 #include <core/text/DcfParser.hpp>
25
26 #include <session/SessionModuleContext.hpp>
27
28 using namespace rstudio::core;
29
30 namespace rstudio {
31 namespace session {
32 namespace modules {
33 namespace ppe {
34
parseDcfResourceFile(const FilePath & resourcePath,boost::function<Error (const std::map<std::string,std::string> &)> callback)35 Error parseDcfResourceFile(
36 const FilePath& resourcePath,
37 boost::function<Error(const std::map<std::string, std::string>&)> callback)
38 {
39 Error error;
40
41 // read dcf contents
42 std::string contents;
43 error = core::readStringFromFile(resourcePath, &contents, string_utils::LineEndingPosix);
44 if (error)
45 return error;
46
47 // attempt to parse as DCF -- multiple newlines used to separate records
48 try
49 {
50 boost::regex reSeparator("\\n{2,}");
51 boost::sregex_token_iterator it(contents.begin(), contents.end(), reSeparator, -1);
52 boost::sregex_token_iterator end;
53
54 for (; it != end; ++it)
55 {
56 // invoke parser on current record
57 std::map<std::string, std::string> fields;
58 std::string errorMessage;
59 error = text::parseDcfFile(*it, true, &fields, &errorMessage);
60 if (error)
61 return error;
62
63 // invoke callback on parsed dcf fields
64 error = callback(fields);
65 if (error)
66 return error;
67 }
68 }
69 CATCH_UNEXPECTED_EXCEPTION;
70
71 return Success();
72 }
73
Indexer()74 Indexer::Indexer() : index_(0), n_(0), running_(false) {}
75
addWorker(boost::shared_ptr<Worker> pWorker)76 void Indexer::addWorker(boost::shared_ptr<Worker> pWorker)
77 {
78 workers_.push_back(pWorker);
79 }
80
removeWorker(boost::shared_ptr<Worker> pWorker)81 void Indexer::removeWorker(boost::shared_ptr<Worker> pWorker)
82 {
83 core::algorithm::expel(workers_, pWorker);
84 }
85
start()86 void Indexer::start()
87 {
88 if (running_)
89 return;
90
91 running_ = true;
92 beginIndexing();
93 module_context::scheduleIncrementalWork(
94 boost::posix_time::milliseconds(300),
95 boost::posix_time::milliseconds(20),
96 boost::bind(&Indexer::work, this),
97 true);
98 }
99
work()100 bool Indexer::work()
101 {
102 // check whether we've run out of work items
103 if (index_ == n_)
104 {
105 endIndexing();
106 return false;
107 }
108
109 std::size_t index = index_++;
110
111 // invoke workers with package name + path
112 FilePath pkgPath = pkgDirs_[index];
113 std::string pkgName = pkgPath.getFilename();
114 for (boost::shared_ptr<Worker> pWorker : workers_)
115 {
116 FilePath resourcePath = pkgPath.completeChildPath(pWorker->resourcePath());
117 if (!resourcePath.exists())
118 continue;
119
120 try
121 {
122 pWorker->onWork(pkgName, resourcePath);
123 }
124 CATCH_UNEXPECTED_EXCEPTION
125 }
126 return true;
127 }
128
beginIndexing()129 void Indexer::beginIndexing()
130 {
131 // reset indexer state
132 pkgDirs_.clear();
133 index_ = 0;
134
135 // discover packages available on the current library paths
136 std::vector<core::FilePath> libPaths = module_context::getLibPaths();
137 for (const core::FilePath& libPath : libPaths)
138 {
139 if (!libPath.exists())
140 continue;
141
142 std::vector<core::FilePath> pkgPaths;
143 core::Error error = libPath.getChildren(pkgPaths);
144 if (error)
145 LOG_ERROR(error);
146
147 pkgDirs_.insert(
148 pkgDirs_.end(),
149 pkgPaths.begin(),
150 pkgPaths.end());
151 }
152 n_ = pkgDirs_.size();
153
154 for (boost::shared_ptr<Worker> pWorker : workers_)
155 {
156 try
157 {
158 pWorker->onIndexingStarted();
159 }
160 CATCH_UNEXPECTED_EXCEPTION
161 }
162 }
163
endIndexing()164 void Indexer::endIndexing()
165 {
166 running_ = false;
167 payload_.clear();
168
169 for (boost::shared_ptr<Worker> pWorker : workers_)
170 {
171 try
172 {
173 pWorker->onIndexingCompleted(&payload_);
174 }
175 CATCH_UNEXPECTED_EXCEPTION
176 }
177
178 ClientEvent event(
179 client_events::kPackageExtensionIndexingCompleted,
180 payload_);
181
182 module_context::enqueClientEvent(event);
183 }
184
indexer()185 Indexer& indexer()
186 {
187 static Indexer instance;
188 return instance;
189 }
190
191 namespace {
192
reindex()193 void reindex()
194 {
195 indexer().start();
196 }
197
reindexDeferred()198 void reindexDeferred()
199 {
200 module_context::scheduleDelayedWork(
201 boost::posix_time::seconds(1),
202 boost::bind(reindex),
203 true);
204 }
205
onDeferredInit(bool)206 void onDeferredInit(bool)
207 {
208 if (module_context::disablePackages())
209 return;
210
211 reindexDeferred();
212 }
213
onConsoleInput(const std::string & input)214 void onConsoleInput(const std::string& input)
215 {
216 if (module_context::disablePackages())
217 return;
218
219 static const char* const commands[] = {
220 "devtools::install_",
221 "devtools::load_all",
222 "install.packages",
223 "install_github",
224 "load_all",
225 "pak::pkg_install",
226 "pak::pkg_remove",
227 "pkg_install",
228 "pkg_remove",
229 "remotes::install_",
230 "remove.packages",
231 "renv::install",
232 "renv::rebuild",
233 "renv::remove",
234 "renv::restore",
235 "utils::install.packages",
236 "utils::remove.packages",
237 };
238
239 std::string inputTrimmed = boost::algorithm::trim_copy(input);
240 for (const char* command : commands)
241 {
242 if (boost::algorithm::starts_with(inputTrimmed, command))
243 {
244 return reindexDeferred();
245 }
246 }
247 }
248
onLibPathsChanged(const std::vector<std::string> & libPaths)249 void onLibPathsChanged(const std::vector<std::string>& libPaths)
250 {
251 if (module_context::disablePackages())
252 return;
253
254 reindexDeferred();
255 }
256
257 } // end anonymous namespace
258
initialize()259 Error initialize()
260 {
261 using namespace module_context;
262 using boost::bind;
263
264 events().onDeferredInit.connect(onDeferredInit);
265 events().onConsoleInput.connect(onConsoleInput);
266 events().onLibPathsChanged.connect(onLibPathsChanged);
267
268 return Success();
269 }
270
271 } // end namespace ppe
272 } // end namespace modules
273 } // end namespace session
274 } // end namespace rstudio
275