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