Doxygen
Loading...
Searching...
No Matches
dotrunner.cpp
Go to the documentation of this file.
1/******************************************************************************
2*
3* Copyright (C) 1997-2019 by Dimitri van Heesch.
4*
5* Permission to use, copy, modify, and distribute this software and its
6* documentation under the terms of the GNU General Public License is hereby
7* granted. No representations are made about the suitability of this software
8* for any purpose. It is provided "as is" without express or implied warranty.
9* See the GNU General Public License for more details.
10*
11* Documents produced by Doxygen are derivative works derived from the
12* input used in their production; they are not affected by this license.
13*
14*/
15
16// own header
17#include "dotrunner.h"
18
19// standard includes
20#include <algorithm>
21#include <cmath>
22#include <map>
23#include <numeric>
24#include <random>
25#include <set>
26#include <sstream>
27#include <string>
28
29#ifdef _MSC_VER
30#pragma warning( push )
31#pragma warning( disable : 4242 )
32#pragma warning( disable : 4244 )
33#pragma warning( disable : 4996 )
34#pragma warning( disable : 4456 )
35#pragma warning( disable : 4805 )
36#endif
37#if defined(__clang__)
38#pragma clang diagnostic push
39#pragma clang diagnostic ignored "-Wdeprecated-declarations"
40#pragma clang diagnostic ignored "-Wshadow"
41#endif
42#if defined(__GNUC__)
43#pragma GCC diagnostic push
44#pragma GCC diagnostic ignored "-Wshadow"
45#endif
46#include <gunzip.hh>
47#if defined(__GNUC__)
48#pragma GCC diagnostic pop
49#endif
50#if defined(__clang__)
51#pragma clang diagnostic pop
52#endif
53#ifdef _MSC_VER
54#pragma warning( pop )
55#endif
56
57// other includes
58#include "config.h"
59#include "dir.h"
60#include "dot.h"
61#include "doxygen.h"
62#include "message.h"
63#include "portable.h"
64#include "threadpool.h"
65#include "util.h"
66
67// the graphicx LaTeX has a limitation of maximum size of 16384
68// To be on the save side we take it a little bit smaller i.e. 150 inch * 72 dpi
69// It is anyway hard to view these size of images
70#define MAX_LATEX_GRAPH_INCH 150
71#define MAX_LATEX_GRAPH_SIZE (MAX_LATEX_GRAPH_INCH * 72)
72
73//#define DBG(x) printf x
74#define DBG(x) do {} while(0)
75
76//-----------------------------------------------------------------------------------------
77
78// since dot silently reproduces the input file when it does not
79// support the PNG format, we need to check the result.
80static void checkPngResult(const DString &imgName)
81{
82 FILE *f = Portable::fopen(imgName,"rb");
83 if (!f)
84 {
85 err("Could not read image '{}' generated by dot!\n",imgName);
86 return;
87 }
88
89 char data[4];
90 if (fread(data, 1, 4, f) != 4)
91 {
92 err("Could not read image '{}' generated by dot!\n",imgName);
93 fclose(f);
94 return;
95 }
96
97 if (!(data[1] == 'P' && data[2] == 'N' && data[3] == 'G'))
98 {
99 err("Image '{}' produced by dot is not a valid PNG!\n"
100 "You should either select a different format "
101 "(DOT_IMAGE_FORMAT in the config file) or install a more "
102 "recent version of graphviz (1.7+)\n", imgName);
103 }
104
105 fclose(f);
106}
107
108static bool resetPDFSize(const int width,const int height, const DString &base)
109{
110 DString tmpName = base+".tmp";
111 DString patchFile = base+".dot";
112 Dir thisDir;
113 if (!thisDir.rename(patchFile.str(),tmpName.str()))
114 {
115 err("Failed to rename file {} to {}!\n",patchFile,tmpName);
116 return false;
117 }
118 std::ifstream fi = Portable::openInputStream(tmpName);
119 std::ofstream t = Portable::openOutputStream(patchFile);
120 if (!fi.is_open())
121 {
122 err("problem opening file {} for patching!\n",tmpName);
123 thisDir.rename(tmpName.str(),patchFile.str());
124 return false;
125 }
126 if (!t.is_open())
127 {
128 err("problem opening file {} for patching!\n",patchFile);
129 thisDir.rename(tmpName.str(),patchFile.str());
130 return false;
131 }
132 std::string line;
133 while (getline(fi,line)) // foreach line
134 {
135 if (line.find("LATEX_PDF_SIZE") != std::string::npos)
136 {
137 double scale = (width > height ? width : height)/double(MAX_LATEX_GRAPH_INCH);
138 t << " size=\""<<width/scale << "," <<height/scale << "\";\n";
139 }
140 else
141 t << line << "\n";
142 }
143 fi.close();
144 t.close();
145 // remove temporary file
146 thisDir.remove(tmpName.str());
147 return true;
148}
149
150bool DotRunner::readBoundingBox(const DString &fileName,int *width,int *height,bool isEps)
151{
152 std::ifstream f = Portable::openInputStream(fileName);
153 if (!f.is_open())
154 {
155 err("Failed to open file {} for extracting bounding box\n",fileName);
156 return false;
157 }
158
159 // read file contents into string 'contents'
160 std::stringstream buffer;
161 buffer << f.rdbuf();
162 std::string contents = buffer.str();
163
164 // start of bounding box marker we are looking for
165 const std::string boundingBox = isEps ? "%%PageBoundingBox:" : "/MediaBox [";
166
167 // helper routine to extract the bounding boxes width and height
168 auto extractBoundingBox = [&fileName,&boundingBox,&width,&height](const char *s) -> bool
169 {
170 int x=0, y=0;
171 double w=0, h=0;
172 if (sscanf(s+boundingBox.length(),"%d %d %lf %lf",&x,&y,&w,&h)==4)
173 {
174 *width = static_cast<int>(std::ceil(w));
175 *height = static_cast<int>(std::ceil(h));
176 return true;
177 }
178 err("Failed to extract bounding box from generated diagram file {}\n",fileName);
179 return false;
180 };
181
182 // compressed segment start and end markers
183 const std::string streamStart = "stream\n";
184 const std::string streamEnd = "\nendstream";
185
186 auto detectDeflateStreamStart = [&streamStart](const char *s)
187 {
188 size_t len = streamStart.length();
189 bool streamOK = strncmp(s,streamStart.c_str(),len)==0;
190 if (streamOK) // ASCII marker matches, check stream header bytes as well
191 {
192 unsigned short header1 = static_cast<unsigned char>(s[len])<<8; // CMF byte
193 if (header1) // not end of string
194 {
195 unsigned short header = (static_cast<unsigned char>(s[len+1])) | header1; // FLG byte
196 // check for correct header (see https://www.rfc-editor.org/rfc/rfc1950)
197 return ((header&0x8F20)==0x0800) && (header%31)==0;
198 }
199 }
200 return false;
201 };
202
203 const size_t l = contents.length();
204 size_t i=0;
205 while (i<l)
206 {
207 if (!isEps && contents[i]=='s' && detectDeflateStreamStart(&contents[i]))
208 { // compressed stream start
209 int col=17;
210 i+=streamStart.length();
211 const size_t start=i;
212 DBG(("---- start stream at offset %08x\n",(int)i));
213 while (i<l)
214 {
215 if (contents[i]=='\n' && strncmp(&contents[i],streamEnd.c_str(),streamEnd.length())==0)
216 { // compressed block found in range [start..i]
217 DBG(("\n---- end stream at offset %08x\n",(int)i));
218 // decompress it into decompressBuf
219 std::vector<char> decompressBuf;
220 const char *source = &contents[start];
221 const size_t sourceLen = i-start;
222 size_t sourcePos = 0;
223 decompressBuf.reserve(sourceLen*2);
224 auto getter = [source,&sourcePos,sourceLen]() -> int {
225 return sourcePos<sourceLen ? static_cast<unsigned char>(source[sourcePos++]) : EOF;
226 };
227 auto putter = [&decompressBuf](const char c) -> int {
228 decompressBuf.push_back(c); return c;
229 };
230 Deflate(getter,putter);
231 // convert decompression buffer to string
232 std::string s(decompressBuf.begin(), decompressBuf.end());
233 DBG(("decompressed_data=[[[\n%s\n]]]\n",s.c_str()));
234 // search for bounding box marker
235 const size_t idx = s.find(boundingBox);
236 if (idx!=std::string::npos) // found bounding box in uncompressed data
237 {
238 return extractBoundingBox(s.c_str()+idx);
239 }
240 // continue searching after end stream marker
241 i+=streamEnd.length();
242 break;
243 }
244 else // compressed stream character
245 {
246 if (col>16) { col=0; DBG(("\n%08x: ",static_cast<int>(i))); }
247 DBG(("%02x ",static_cast<unsigned char>(contents[i])));
248 col++;
249 i++;
250 }
251 }
252 }
253 else if (((isEps && contents[i]=='%') || (!isEps && contents[i]=='/')) &&
254 strncmp(&contents[i],boundingBox.c_str(),boundingBox.length())==0)
255 { // uncompressed bounding box
256 return extractBoundingBox(&contents[i]);
257 }
258 else // uncompressed stream character
259 {
260 i++;
261 }
262 }
263 err("Failed to find bounding box in generated diagram file {}\n",fileName);
264 // nothing found
265 return false;
266}
267
268//---------------------------------------------------------------------------------
269
271{
272 size_t index = output.rfind('.');
273 if (index==DString::npos) return output;
274 return output.left(index);
275}
276
278 : m_dotExe(Doxygen::verifiedDotPath)
279{
280}
281
282bool DotRunner::run(const DotJobs &dotJobs)
283{
284 if (dotJobs.empty()) return true;
285
286 // Group jobs by format, then by directory so we can cd once per group
287 std::map<std::string, std::map<std::string, std::vector<const DotJob*>>> byFormatAndDir;
288 for (const auto &job : dotJobs)
289 {
290 byFormatAndDir[job.format.str()][job.absPath.str()].push_back(&job);
291 }
292
293 std::mt19937 rng(std::random_device{}());
294 bool ok = true;
295 size_t prev=0;
296 for (const auto &[fmtStr, byDir] : byFormatAndDir)
297 {
298 DString format = DString(fmtStr);
299
300 for (const auto &[dirStr, jobs] : byDir)
301 {
302 std::string oldDir = Dir::currentDirPath();
303 Dir::setCurrent(dirStr);
304
305 // settings controlling how to distribute the graphs over threads and batches
306 const size_t numThreads = static_cast<size_t>(Config_getInt(DOT_NUM_THREADS));
307 const size_t batchSize = static_cast<size_t>(Config_getInt(DOT_BATCH_SIZE));
308 const size_t exeLen = m_dotExe.length() + 1; // "exe " prefix
309 const size_t maxArgLen = 32000-exeLen; // Windows CreateProcess limit is 32767; keep safe margin
310
311 // create a pseudo random ordering in which to process the dot files
312 std::vector<size_t> indices(jobs.size());
313 std::iota(indices.begin(), indices.end(), 0);
314 std::shuffle(indices.begin(), indices.end(), rng);
315
316 // helper to keep track of dot command to run later
317 struct CommandArgument
318 {
319 CommandArgument(const DString &args) : arguments(args) {}
320 DString arguments;
321 size_t numDotFiles = 0;
322 const DotJob *firstJob = nullptr;
323 };
324
325 std::vector<CommandArgument> partialCommands;
326 std::vector<CommandArgument> finalCommands;
327
328 bool hasImageMap = std::any_of(jobs.begin(),jobs.end(),[](const auto &j) { return j->generateImageMap; });
329
330 // each dot command has a command arguments of the form: -Tformat -O basename1.dot basename2.dot ...
331 DString baseArgs = DString("-T") + format;
332 if (hasImageMap) // if any image needs a map we generate one for all images
333 {
334 baseArgs += " -Tcmapx";
335 }
336 baseArgs += " -O";
337
338 // prepare partial commands for each thread (command is later skipped if numDotFiles==0).
339 for (size_t i=0; i<numThreads; i++)
340 {
341 partialCommands.emplace_back(baseArgs);
342 }
343
344 // split the jobs into batches per thread iterating in pseudo random order to fill each batch with a random selection of graphs
345 size_t index=0;
346 for (size_t i : indices)
347 {
348 const auto &job = jobs[i];
349 DString fileArg = DString(" ") + job->relDotName;
350 auto &cmd = partialCommands[index];
351 if (cmd.numDotFiles<batchSize && cmd.arguments.length()+fileArg.length()<maxArgLen) // still room in this batch
352 {
353 cmd.arguments+=fileArg;
354 cmd.numDotFiles++;
355 }
356 else // this batch is full, move to finished commands and start a new one
357 {
358 finalCommands.push_back(cmd);
359 cmd.arguments=baseArgs+fileArg;
360 cmd.numDotFiles=1;
361 }
362 if (cmd.firstJob==nullptr) cmd.firstJob=job;
363 index = (index+1)%numThreads;
364 }
365
366 // append partial commands to the final commands
367 finalCommands.insert(finalCommands.end(),partialCommands.begin(),partialCommands.end());
368
369 // now run the finalCommands.
370 if (Config_getInt(DOT_NUM_THREADS)<=1) // no threads to work with
371 {
372 for (const auto &cmd : finalCommands)
373 {
374 if (cmd.numDotFiles>0) // check if there are graphs to generate first
375 {
376 if (cmd.numDotFiles>1) // batch mode
377 {
378 msg("Running dot for graphs {}-{}/{}\n",prev+1,prev+cmd.numDotFiles,dotJobs.size());
379 }
380 else // single graph mode
381 {
382 msg("Running dot for graph {}/{}\n",prev+1,dotJobs.size());
383 }
384 prev+=cmd.numDotFiles;
385 int exitCode;
386 if ((exitCode = Portable::system(m_dotExe, cmd.arguments, false)) != 0)
387 {
388 err_full(cmd.firstJob->srcFile, 1,
389 "Problems running dot: exit code={}, command='{}', dir='{}', arguments='{}'",
390 exitCode, m_dotExe, dirStr, cmd.arguments);
391 ok = false;
392 }
393 }
394 }
395 }
396 else // use multiple threads to run instances of dot in parallel
397 {
398 ThreadPool workers(numThreads);
399 std::vector< std::future<size_t> > results;
400 for (auto & cmd: finalCommands)
401 {
402 if (cmd.numDotFiles>0)
403 {
404 auto locDirStr = dirStr;
405 auto process = [this,cmd,locDirStr]() -> size_t
406 {
407 int exitCode;
408 if ((exitCode = Portable::system(m_dotExe, cmd.arguments, false)) != 0)
409 {
410 err_full(cmd.firstJob->srcFile, 1,
411 "Problems running dot: exit code={}, command='{}', dir='{}', arguments='{}'",
412 exitCode, m_dotExe, locDirStr, cmd.arguments);
413 }
414 return cmd.numDotFiles;
415 };
416 results.emplace_back(workers.queue(process));
417 }
418 }
419 for (auto &f : results)
420 {
421 size_t numDotFiles = f.get();
422 if (numDotFiles>1) // batch mode
423 {
424 msg("Finished running dot for graphs {}-{}/{}\n",prev+1,prev+numDotFiles,dotJobs.size());
425 }
426 else // single graph mode
427 {
428 msg("Finished running dot for graph {}/{}\n",prev+1,dotJobs.size());
429 }
430 prev+=numDotFiles;
431 }
432 }
433
434 // Post-process each output file. dot -O appends the format suffix to the
435 // full input filename, so the output is absPath + relDotName + "." + format.
436 // Rename to remove the .dot infix, producing absPath + baseName + "." + format.
437 for (const auto *job : jobs)
438 {
439 DString base = job->absPath + getBaseNameOfOutput(job->relDotName);
440 DString dotOutput = job->absPath + job->relDotName + "." + format;
441 DString output = base + "." + format;
442 Dir d;
443 if (!d.rename(dotOutput.str(), output.str()))
444 {
445 err("Failed to rename {} to {}!\n", dotOutput, output);
446 ok = false;
447 continue;
448 }
449 if (job->generateImageMap)
450 {
451 DString dotMapOutput = job->absPath + job->relDotName + ".cmapx";
452 DString mapOutput = base + ".map";
453 if (!d.rename(dotMapOutput.str(), mapOutput.str()))
454 {
455 err("Failed to rename {} to {}!\n", dotMapOutput, mapOutput);
456 ok = false;
457 continue;
458 }
459 }
460
461 if (format.startsWith("pdf"))
462 {
463 int width=0, height=0;
464 if (!readBoundingBox(output, &width, &height, false))
465 {
466 ok = false;
467 continue;
468 }
469 if ((width > MAX_LATEX_GRAPH_SIZE) || (height > MAX_LATEX_GRAPH_SIZE))
470 {
471 if (!resetPDFSize(width, height, base))
472 {
473 ok = false;
474 continue;
475 }
476 // Re-run dot for just this one file
477 DString rerunArgs = DString("-T") + format + " -O \"" + job->relDotName + "\"";
478 int exitCode;
479 if ((exitCode = Portable::system(m_dotExe, rerunArgs, false)) != 0)
480 {
481 err_full(job->srcFile, 1,
482 "Problems running dot: exit code={}, command='{}', dir='{}', arguments='{}'",
483 exitCode, m_dotExe, dirStr, rerunArgs);
484 ok = false;
485 }
486 else
487 {
488 Dir d2;
489 if (!d2.rename(dotOutput.str(), output.str()))
490 {
491 err("Failed to rename {} to {}!\n", dotOutput, output);
492 ok = false;
493 }
494 }
495 }
496 }
497 else if (format.startsWith("png"))
498 {
499 checkPngResult(output);
500 }
501 }
502 Dir::setCurrent(oldDir);
503 }
504 }
505
506 // Write .md5 files and clean up .dot files (once per unique dotFile)
507 std::set<std::string> processed;
508 for (const auto &job : dotJobs)
509 {
510 if (!processed.insert((job.absPath + job.relDotName).str()).second) continue;
511
512 if (!job.md5Hash.empty())
513 {
514 DString md5Name = job.absPath + getBaseNameOfOutput(job.relDotName) + ".md5";
515 FILE *f = Portable::fopen(md5Name, "w");
516 if (f)
517 {
518 fwrite(job.md5Hash.data(), 1, 32, f);
519 fclose(f);
520 }
521 }
522
523 if (Config_getBool(DOT_CLEANUP))
524 {
525 Portable::unlink(job.absPath + job.relDotName);
526 }
527 }
528
529 return ok;
530}
A String class for use with Doxygen wrapping std::string and adding some additional functionality off...
Definition dstring.h:84
size_t rfind(char c, size_t pos=npos) const
Definition dstring.h:244
static constexpr size_t npos
value used to indicate 'not found' or 'to the end of the string', matching std::string::npos
Definition dstring.h:178
DString left(size_t len) const
Definition dstring.h:306
const std::string & str() const
Definition dstring.h:645
size_t length() const
Returns the length of the string, not counting the 0-terminator.
Definition dstring.h:151
Class representing a directory in the file system.
Definition dir.h:73
static std::string currentDirPath()
Definition dir.cpp:348
bool remove(const std::string &path, bool acceptsAbsPath=true) const
Definition dir.cpp:320
bool rename(const std::string &orgName, const std::string &newName, bool acceptsAbsPath=true) const
Definition dir.cpp:327
static bool setCurrent(const std::string &path)
Definition dir.cpp:356
static bool readBoundingBox(const DString &fileName, int *width, int *height, bool isEps)
DString m_dotExe
Definition dotrunner.h:42
bool run(const DotJobs &jobs)
Runs dot for all given jobs.
This class serves as a namespace for global variables used by doxygen.
Definition doxygen.h:86
Class managing a pool of worker threads.
Definition threadpool.h:48
auto queue(F &&f, Args &&... args) -> std::future< decltype(f(args...))>
Queue the callable function f for the threads to execute.
Definition threadpool.h:77
#define Config_getInt(name)
Definition config.h:34
#define Config_getBool(name)
Definition config.h:33
std::vector< DotJob > DotJobs
Definition dotjob.h:37
#define MAX_LATEX_GRAPH_INCH
Definition dotrunner.cpp:70
#define DBG(x)
Definition dotrunner.cpp:74
static void checkPngResult(const DString &imgName)
Definition dotrunner.cpp:80
#define MAX_LATEX_GRAPH_SIZE
Definition dotrunner.cpp:71
static DString getBaseNameOfOutput(const DString &output)
static bool resetPDFSize(const int width, const int height, const DString &base)
#define msg(fmt,...)
Definition message.h:94
#define err(fmt,...)
Definition message.h:127
#define err_full(file, line, fmt,...)
Definition message.h:132
std::ifstream openInputStream(const DString &name, bool binary=false, bool openAtEnd=false)
Definition portable.cpp:692
int system(const DString &command, const DString &args, bool commandHasConsole=true)
Definition portable.cpp:121
void unlink(const DString &fileName)
Definition portable.cpp:560
std::ofstream openOutputStream(const DString &name, bool append=false)
Definition portable.cpp:681
FILE * fopen(const DString &fileName, const DString &mode)
Definition portable.cpp:365
Portable versions of functions that are platform dependent.
A bunch of utility functions.