-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathGraphvizCommand.cs
More file actions
142 lines (123 loc) · 5.84 KB
/
GraphvizCommand.cs
File metadata and controls
142 lines (123 loc) · 5.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Runtime.InteropServices;
using System.Linq;
namespace Rubjerg.Graphviz;
/// <summary>
/// See https://graphviz.org/doc/info/command.html
/// </summary>
public class GraphvizCommand
{
internal static string Rid
{
get
{
var os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx"
: "linux"; // default
var arch = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X64 => "x64",
Architecture.Arm64 => "arm64",
Architecture.X86 => "x86",
Architecture.Arm => "arm",
// Cast allows compilation in .NET Standard 2.0/2.1.
// (Architecture)5 is S390x, added in .NET 6.
(Architecture)5 => "s390x",
_ => "unknown"
};
return $"{os}-{arch}";
}
}
internal static readonly Lazy<string> _DotExePath = new Lazy<string>(() =>
{
// Depending on the method of deployment, there are several possible directories to look for dot
// The DOT_BINARY_PATH environment variable can be set to point to a specific location, which will be checked first.
var dotBinaryPath = Environment.GetEnvironmentVariable("DOT_BINARY_PATH");
var possibleLocations = new List<string>();
if (!string.IsNullOrEmpty(dotBinaryPath))
{
possibleLocations.Add(dotBinaryPath);
}
possibleLocations.Add(Path.Combine(AppContext.BaseDirectory, "runtimes", Rid, "native"));
possibleLocations.Add(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
possibleLocations.Add(Path.GetDirectoryName(AppContext.BaseDirectory));
possibleLocations.Add("");
return possibleLocations.Where(d => d != null).Select(dir => Path.Combine(dir, "dot")).FirstOrDefault(File.Exists)
?? possibleLocations.Where(d => d != null).Select(dir => Path.Combine(dir, "dot.exe")).FirstOrDefault(File.Exists)
?? throw new InvalidOperationException("Could not find path to dot binary in any of: " + string.Join(", ", possibleLocations));
});
internal static string DotExePath => _DotExePath.Value;
public static RootGraph CreateLayout(Graph input, string engine = LayoutEngines.Dot, CoordinateSystem coordinateSystem = CoordinateSystem.BottomLeft)
{
var (stdout, stderr) = Exec(input, engine: engine);
var stdoutStr = ConvertBytesOutputToString(stdout);
var resultGraph = RootGraph.FromDotString(stdoutStr, coordinateSystem);
resultGraph.Warnings = stderr;
return resultGraph;
}
public static string ConvertBytesOutputToString(byte[] data)
{
// Just to be safe, make sure the input has unix line endings. Graphviz does not properly support
// windows line endings passed to stdin when it comes to attribute line continuations.
return Encoding.UTF8.GetString(data).Replace("\r\n", "\n");
}
/// <summary>
/// Start dot.exe to compute a layout.
/// </summary>
/// <exception cref="ApplicationException">When the Graphviz process did not return successfully</exception>
/// <returns>stderr may contain warnings, stdout is in utf8 encoding</returns>
public static (byte[] stdout, string stderr) Exec(Graph input, string format = "xdot", string? outputPath = null, string engine = LayoutEngines.Dot)
{
string arguments = $"-T{format} -K{engine}";
if (outputPath != null)
{
arguments = $"{arguments} -o\"{outputPath}\"";
}
string? inputToStdin = input.ToDotString();
Process process = new Process();
process.StartInfo.FileName = DotExePath;
process.StartInfo.Arguments = arguments;
// Redirect the input/output streams
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
// In some situations starting a new process also starts a new console window, which is distracting and causes slowdown.
// This flag prevents this from happening.
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
StringBuilder stderr = new StringBuilder();
process.ErrorDataReceived += (_, e) => stderr.AppendLine(e.Data);
_ = process.Start();
process.BeginErrorReadLine();
// Write to stdin
var inputBytes = Encoding.UTF8.GetBytes(inputToStdin);
using (var sw = process.StandardInput.BaseStream)
sw.Write(inputBytes, 0, inputBytes.Length);
// Read from stdout, can be binary output such as pdf
byte[] stdout;
using (MemoryStream memoryStream = new MemoryStream())
{
process.StandardOutput.BaseStream.CopyTo(memoryStream);
stdout = memoryStream.ToArray();
}
process.WaitForExit();
if (process.ExitCode != 0)
{
// Something went wrong.
throw new ApplicationException($"Process exited with code {process.ExitCode}. Error details: {stderr}");
}
else
{
// Process completed successfully.
// Let's use unix line endings for consistency with stdout
return (stdout, stderr.ToString().Replace("\r\n", "\n"));
}
}
}