Skip to content

Commit 4cd386b

Browse files
committed
Initial commit
#1
1 parent 2db9d73 commit 4cd386b

8 files changed

Lines changed: 458 additions & 0 deletions

Plugins.ExternalProcess.sln

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.12.35728.132
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlowSynx.Plugins.ExternalProcess", "src\FlowSynx.Plugins.ExternalProcess.csproj", "{F29B6A87-475E-4081-9C2D-CCF8CDD9E6C6}"
7+
EndProject
8+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{ADB44AEC-415B-4CF1-B0CA-92E8792716C6}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{F29B6A87-475E-4081-9C2D-CCF8CDD9E6C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{F29B6A87-475E-4081-9C2D-CCF8CDD9E6C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{F29B6A87-475E-4081-9C2D-CCF8CDD9E6C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{F29B6A87-475E-4081-9C2D-CCF8CDD9E6C6}.Release|Any CPU.Build.0 = Release|Any CPU
20+
EndGlobalSection
21+
GlobalSection(SolutionProperties) = preSolution
22+
HideSolutionNode = FALSE
23+
EndGlobalSection
24+
GlobalSection(NestedProjects) = preSolution
25+
{F29B6A87-475E-4081-9C2D-CCF8CDD9E6C6} = {ADB44AEC-415B-4CF1-B0CA-92E8792716C6}
26+
EndGlobalSection
27+
EndGlobal

src/ExternalProcessPlugin.cs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
using FlowSynx.PluginCore;
2+
using FlowSynx.PluginCore.Extensions;
3+
using FlowSynx.PluginCore.Helpers;
4+
using FlowSynx.Plugins.ExternalProcess.Models;
5+
using System.Diagnostics;
6+
using System.Text;
7+
8+
namespace FlowSynx.Plugins.ExternalProcess;
9+
10+
public class ExternalProcessPlugin : IPlugin
11+
{
12+
private IPluginLogger? _logger;
13+
private bool _isInitialized;
14+
15+
public PluginMetadata Metadata => new()
16+
{
17+
Id = Guid.Parse("df657e78-10ae-4e61-beda-2e4f9b1b6a7a"),
18+
Name = "ExternalProcess",
19+
CompanyName = "FlowSynx",
20+
Description = Resources.PluginDescription,
21+
Version = new Version(1, 0, 0),
22+
Category = PluginCategory.Execution,
23+
Authors = new List<string> { "FlowSynx" },
24+
Copyright = "© FlowSynx. All rights reserved.",
25+
Icon = "flowsynx.png",
26+
ReadMe = "README.md",
27+
RepositoryUrl = "https://github.com/flowsynx/plugin-external-process",
28+
ProjectUrl = "https://flowsynx.io",
29+
Tags = new List<string>() { "flowsynx", "execution", "external-process", "plugin" },
30+
MinimumFlowSynxVersion = new Version(1, 1, 1),
31+
};
32+
33+
public PluginSpecifications? Specifications { get; set; }
34+
35+
public Type SpecificationsType => typeof(ExternalProcessPluginSpecifications);
36+
37+
public IReadOnlyCollection<string> SupportedOperations => new List<string>();
38+
39+
public Task Initialize(IPluginLogger logger)
40+
{
41+
if (ReflectionHelper.IsCalledViaReflection())
42+
throw new InvalidOperationException(Resources.ReflectionBasedAccessIsNotAllowed);
43+
44+
ArgumentNullException.ThrowIfNull(logger);
45+
_logger = logger;
46+
_isInitialized = true;
47+
return Task.CompletedTask;
48+
}
49+
50+
public async Task<object?> ExecuteAsync(PluginParameters parameters, CancellationToken cancellationToken)
51+
{
52+
cancellationToken.ThrowIfCancellationRequested();
53+
54+
if (ReflectionHelper.IsCalledViaReflection())
55+
throw new InvalidOperationException(Resources.ReflectionBasedAccessIsNotAllowed);
56+
57+
if (!_isInitialized)
58+
throw new InvalidOperationException($"Plugin '{Metadata.Name}' v{Metadata.Version} is not initialized.");
59+
60+
var inputParameter = parameters.ToObject<InputParameter>();
61+
return await ExecuteProcessAsync(inputParameter, cancellationToken);
62+
}
63+
64+
#region private methods
65+
private async Task<PluginContext> ExecuteProcessAsync(
66+
InputParameter inputParameter,
67+
CancellationToken cancellationToken)
68+
{
69+
if (_logger == null)
70+
{
71+
throw new InvalidOperationException("Plugin is not initialized. Call Initialize() first.");
72+
}
73+
74+
_logger.LogInfo($"Starting process: {inputParameter.FileName} {inputParameter.Arguments}");
75+
76+
var processStartInfo = new ProcessStartInfo
77+
{
78+
FileName = inputParameter.FileName,
79+
Arguments = inputParameter.Arguments,
80+
WorkingDirectory = string.IsNullOrWhiteSpace(inputParameter.WorkingDirectory)
81+
? Environment.CurrentDirectory
82+
: inputParameter.WorkingDirectory,
83+
RedirectStandardOutput = true,
84+
RedirectStandardError = true,
85+
UseShellExecute = false,
86+
CreateNoWindow = !inputParameter.ShowWindow
87+
};
88+
89+
var outputBuilder = new StringBuilder();
90+
var errorBuilder = new StringBuilder();
91+
92+
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
93+
94+
process.OutputDataReceived += (_, e) =>
95+
{
96+
if (e.Data != null)
97+
{
98+
outputBuilder.AppendLine(e.Data);
99+
_logger.LogDebug($"STDOUT: {e.Data}");
100+
}
101+
};
102+
103+
process.ErrorDataReceived += (_, e) =>
104+
{
105+
if (e.Data != null)
106+
{
107+
errorBuilder.AppendLine(e.Data);
108+
_logger.LogError($"STDERR: {e.Data}");
109+
}
110+
};
111+
112+
var startTime = DateTime.UtcNow;
113+
114+
process.Start();
115+
process.BeginOutputReadLine();
116+
process.BeginErrorReadLine();
117+
118+
await Task.Run(() =>
119+
{
120+
while (!process.HasExited)
121+
{
122+
cancellationToken.ThrowIfCancellationRequested();
123+
Thread.Sleep(100);
124+
}
125+
}, cancellationToken);
126+
127+
await process.WaitForExitAsync(cancellationToken);
128+
var endTime = DateTime.UtcNow;
129+
130+
var stdout = outputBuilder.ToString();
131+
var stderr = errorBuilder.ToString();
132+
var combinedOutput = stdout + stderr;
133+
134+
if (process.ExitCode != 0)
135+
{
136+
_logger.LogError($"Process failed with exit code {process.ExitCode}");
137+
if (inputParameter.FailOnNonZeroExit)
138+
throw new Exception($"Process failed with exit code {process.ExitCode}. Error: {stderr}");
139+
else
140+
_logger.LogWarning("Continuing despite process failure because FailOnNonZeroExit is false.");
141+
}
142+
else
143+
{
144+
_logger.LogInfo($"Process completed successfully with exit code {process.ExitCode}");
145+
}
146+
147+
var context = new PluginContext(Guid.NewGuid().ToString(), "Execution")
148+
{
149+
Format = "Text",
150+
Content = combinedOutput,
151+
StructuredData = null
152+
};
153+
context.Metadata["ExitCode"] = process.ExitCode;
154+
context.Metadata["StartTimeUtc"] = startTime;
155+
context.Metadata["EndTimeUtc"] = endTime;
156+
context.Metadata["Duration"] = endTime - startTime;
157+
context.Metadata["WasSuccessful"] = process.ExitCode == 0;
158+
context.Metadata["StdErrPresent"] = !string.IsNullOrWhiteSpace(stderr);
159+
160+
return context;
161+
}
162+
#endregion
163+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<Compile Remove=".template.config\**" />
11+
<EmbeddedResource Remove=".template.config\**" />
12+
<None Remove=".template.config\**" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="FlowSynx.PluginCore" Version="1.3.3" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<Compile Update="Resources.Designer.cs">
21+
<DesignTime>True</DesignTime>
22+
<AutoGen>True</AutoGen>
23+
<DependentUpon>Resources.resx</DependentUpon>
24+
</Compile>
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<EmbeddedResource Update="Resources.resx">
29+
<Generator>ResXFileCodeGenerator</Generator>
30+
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
31+
</EmbeddedResource>
32+
</ItemGroup>
33+
34+
<ItemGroup>
35+
<None Update="flowsynx.png">
36+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
37+
</None>
38+
<None Update="README.md">
39+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
40+
</None>
41+
</ItemGroup>
42+
43+
</Project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using FlowSynx.PluginCore;
2+
3+
namespace FlowSynx.Plugins.ExternalProcess.Models;
4+
5+
public class ExternalProcessPluginSpecifications : PluginSpecifications
6+
{
7+
8+
}

src/Models/InputParameter.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace FlowSynx.Plugins.ExternalProcess.Models;
2+
3+
internal class InputParameter
4+
{
5+
public string FileName { get; set; } = string.Empty;
6+
public string Arguments { get; set; } = string.Empty;
7+
public string? WorkingDirectory { get; set; }
8+
public bool ShowWindow { get; set; } = false;
9+
public bool FailOnNonZeroExit { get; set; } = true;
10+
}

src/Resources.Designer.cs

Lines changed: 81 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)