Skip to content

Commit 06a2876

Browse files
authored
Merge branch 'main' into fix/payment-status-read-409-locked-data
2 parents 6f51857 + b86fb54 commit 06a2876

15 files changed

Lines changed: 637 additions & 28 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
using System.Diagnostics;
2+
using System.Text.Json;
3+
4+
namespace Altinn.App.Api.Extensions;
5+
6+
internal static class StudioctlLocalConfiguration
7+
{
8+
private const string StudioctlAppRunEnvironmentVariable = "STUDIOCTL_APP_RUN";
9+
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(3);
10+
private static readonly IReadOnlyDictionary<string, string?> _emptyConfiguration =
11+
new Dictionary<string, string?>();
12+
13+
internal static void AddIfAvailable(IConfigurationBuilder configBuilder, IHostEnvironment hostEnvironment)
14+
{
15+
try
16+
{
17+
AddIfAvailableCore(configBuilder, hostEnvironment);
18+
}
19+
catch (Exception ex)
20+
{
21+
WriteWarning("Failed to import local studioctl app configuration.", ex);
22+
}
23+
}
24+
25+
private static void AddIfAvailableCore(IConfigurationBuilder configBuilder, IHostEnvironment hostEnvironment)
26+
{
27+
if (configBuilder is null || !ShouldAdd(hostEnvironment))
28+
{
29+
return;
30+
}
31+
32+
string contentRootPath = hostEnvironment.ContentRootPath;
33+
IReadOnlyDictionary<string, string?> env = TryReadStudioctlEnvironment(
34+
FindProjectPath(contentRootPath) ?? contentRootPath,
35+
_defaultTimeout
36+
);
37+
if (env.Count == 0)
38+
{
39+
return;
40+
}
41+
42+
configBuilder.AddInMemoryCollection(NormalizeConfigurationKeys(env));
43+
}
44+
45+
internal static bool ShouldAdd(IHostEnvironment? hostEnvironment)
46+
{
47+
if (hostEnvironment is null)
48+
{
49+
return false;
50+
}
51+
52+
if (!hostEnvironment.IsDevelopment())
53+
{
54+
return false;
55+
}
56+
57+
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(StudioctlAppRunEnvironmentVariable)))
58+
{
59+
return false;
60+
}
61+
62+
string contentRootPath = hostEnvironment.ContentRootPath;
63+
return !string.IsNullOrWhiteSpace(contentRootPath) && Directory.Exists(contentRootPath);
64+
}
65+
66+
internal static IReadOnlyDictionary<string, string?> TryReadStudioctlEnvironment(
67+
string projectOrRootPath,
68+
TimeSpan timeout
69+
)
70+
{
71+
// Contract with studioctl: `studioctl app env --json` returns a flat JSON object
72+
// where keys are environment variable names and values are environment variable values.
73+
return
74+
TryRunStudioctlEnvironmentCommand(projectOrRootPath, timeout, out string json)
75+
&& TryParseEnvironmentJson(json, out Dictionary<string, string?> values)
76+
? values
77+
: _emptyConfiguration;
78+
}
79+
80+
internal static bool TryParseEnvironmentJson(string json, out Dictionary<string, string?> values)
81+
{
82+
values = [];
83+
84+
if (string.IsNullOrWhiteSpace(json))
85+
{
86+
return false;
87+
}
88+
89+
JsonDocument document;
90+
try
91+
{
92+
document = JsonDocument.Parse(json);
93+
}
94+
catch (Exception ex)
95+
{
96+
WriteWarning("studioctl app env returned invalid JSON.", ex);
97+
return false;
98+
}
99+
using (document)
100+
{
101+
if (document.RootElement.ValueKind != JsonValueKind.Object)
102+
{
103+
return false;
104+
}
105+
106+
values = new(StringComparer.OrdinalIgnoreCase);
107+
foreach (JsonProperty property in document.RootElement.EnumerateObject())
108+
{
109+
if (property.Value.ValueKind == JsonValueKind.String)
110+
{
111+
values[property.Name] = property.Value.GetString();
112+
}
113+
}
114+
}
115+
116+
return true;
117+
}
118+
119+
internal static Dictionary<string, string?> NormalizeConfigurationKeys(IReadOnlyDictionary<string, string?> values)
120+
{
121+
Dictionary<string, string?> normalized = new(StringComparer.OrdinalIgnoreCase);
122+
foreach ((string key, string? value) in values)
123+
{
124+
normalized[key.Replace("__", ":", StringComparison.Ordinal)] = value;
125+
}
126+
127+
return normalized;
128+
}
129+
130+
internal static ProcessStartInfo CreateStartInfo(string projectOrRootPath)
131+
{
132+
ProcessStartInfo startInfo = new()
133+
{
134+
FileName = "studioctl",
135+
RedirectStandardError = true,
136+
RedirectStandardOutput = true,
137+
UseShellExecute = false,
138+
CreateNoWindow = true,
139+
};
140+
141+
startInfo.ArgumentList.Add("app");
142+
startInfo.ArgumentList.Add("env");
143+
startInfo.ArgumentList.Add("--json");
144+
startInfo.ArgumentList.Add("--project");
145+
startInfo.ArgumentList.Add(projectOrRootPath);
146+
147+
return startInfo;
148+
}
149+
150+
private static bool TryRunStudioctlEnvironmentCommand(string projectOrRootPath, TimeSpan timeout, out string output)
151+
{
152+
output = string.Empty;
153+
using Process process = new() { StartInfo = CreateStartInfo(projectOrRootPath) };
154+
155+
if (!process.Start())
156+
{
157+
WriteWarning("studioctl app env did not start.");
158+
return false;
159+
}
160+
161+
if (!process.WaitForExit(timeout))
162+
{
163+
TryKill(process);
164+
WriteWarning($"studioctl app env timed out after {timeout.TotalSeconds} seconds.");
165+
return false;
166+
}
167+
168+
output = process.StandardOutput.ReadToEnd();
169+
string errorOutput = process.StandardError.ReadToEnd().Trim();
170+
171+
if (process.ExitCode == 0)
172+
{
173+
return true;
174+
}
175+
176+
string details = string.IsNullOrWhiteSpace(errorOutput) ? "." : $": {errorOutput}";
177+
WriteWarning($"studioctl app env exited with code {process.ExitCode}{details}");
178+
return false;
179+
}
180+
181+
private static string? FindProjectPath(string contentRootPath)
182+
{
183+
return Directory
184+
.EnumerateFiles(contentRootPath, "*.csproj", SearchOption.TopDirectoryOnly)
185+
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
186+
.FirstOrDefault();
187+
}
188+
189+
private static void TryKill(Process process)
190+
{
191+
try
192+
{
193+
process.Kill(entireProcessTree: true);
194+
}
195+
catch (Exception ex)
196+
{
197+
WriteWarning("Failed to stop timed out studioctl app env process.", ex);
198+
}
199+
}
200+
201+
private static void WriteWarning(string message, Exception? exception = null)
202+
{
203+
if (exception is null)
204+
{
205+
Console.Error.WriteLine($"Warning: {message}");
206+
return;
207+
}
208+
209+
Console.Error.WriteLine($"Warning: {message}{Environment.NewLine}{exception}");
210+
}
211+
}

src/Altinn.App.Api/Extensions/WebHostBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public static void ConfigureAppWebHost(this IWebHostBuilder builder, string[] ar
3131
}
3232

3333
configBuilder.AddInMemoryCollection(config);
34+
StudioctlLocalConfiguration.AddIfAvailable(configBuilder, context.HostingEnvironment);
3435

3536
var runtimeSecretsDirectory = context.Configuration["AppSettings:RuntimeSecretsDirectory"];
3637
if (string.IsNullOrWhiteSpace(runtimeSecretsDirectory))

src/Altinn.App.Core/Features/IInstanceDataAccessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public interface IInstanceDataAccessor
7676
/// <see cref="IInstanceDataAccessor"/> directly in the expression evaluator.
7777
/// </summary>
7878
/// <returns></returns>
79-
internal LayoutEvaluatorState? GetLayoutEvaluatorState();
79+
internal LayoutEvaluatorState GetLayoutEvaluatorState();
8080

8181
/// <summary>
8282
/// <para>Set the authentication method used when reading and writing data of the given data type.</para>

src/Altinn.App.Core/Internal/Data/CleanInstanceDataAccessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public DataElement GetDataElement(DataElementIdentifier dataElementIdentifier)
144144
return _dataAccessor.GetDataElement(dataElementIdentifier);
145145
}
146146

147-
public LayoutEvaluatorState? GetLayoutEvaluatorState()
147+
public LayoutEvaluatorState GetLayoutEvaluatorState()
148148
{
149149
throw new NotImplementedException(
150150
"GetLayoutEvaluatorState is not implemented in CleanInstanceDataAccessor, because LayoutEvaluatorState will be deprecated."

src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,19 +181,15 @@ public IInstanceDataAccessor GetPreviousDataAccessor()
181181

182182
private LayoutEvaluatorState? _layoutEvaluatorStateCache;
183183

184-
public LayoutEvaluatorState? GetLayoutEvaluatorState()
184+
public LayoutEvaluatorState GetLayoutEvaluatorState()
185185
{
186-
if (TaskId is null)
187-
{
188-
return null;
189-
}
190186
if (_layoutEvaluatorStateCache is not null)
191187
{
192188
return _layoutEvaluatorStateCache;
193189
}
194190

195191
// Could use a double lock here, but a deadlock is more problematic than creating the state twice
196-
var layouts = _appResources.GetLayoutModelForTask(TaskId);
192+
var layouts = TaskId is null ? null : _appResources.GetLayoutModelForTask(TaskId);
197193

198194
_layoutEvaluatorStateCache = new LayoutEvaluatorState(
199195
this,

src/Altinn.App.Core/Internal/Data/PreviousDataAccessor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal class PreviousDataAccessor : IInstanceDataAccessor
1919
private readonly FrontEndSettings _frontEndSettings;
2020
private readonly ITranslationService _translationService;
2121
private readonly Telemetry? _telemetry;
22-
private readonly Lazy<LayoutEvaluatorState?> _layoutEvaluatorState;
22+
private readonly Lazy<LayoutEvaluatorState> _layoutEvaluatorState;
2323

2424
private readonly ConcurrentDictionary<DataElementIdentifier, Task<IFormDataWrapper>> _previousDataCache = new();
2525

@@ -41,7 +41,7 @@ public PreviousDataAccessor(
4141
_layoutEvaluatorState = new(() =>
4242
{
4343
var originalState = _dataAccessor.GetLayoutEvaluatorState();
44-
return originalState?.WithDataAccessor(this);
44+
return originalState.WithDataAccessor(this);
4545
});
4646
}
4747

@@ -106,7 +106,7 @@ public IInstanceDataAccessor GetPreviousDataAccessor()
106106
return this;
107107
}
108108

109-
public LayoutEvaluatorState? GetLayoutEvaluatorState()
109+
public LayoutEvaluatorState GetLayoutEvaluatorState()
110110
{
111111
return _layoutEvaluatorState.Value;
112112
}

src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ public async Task<string> TranslateText(string textKey, ComponentContext context
445445
return _componentModel?.DefaultDataType;
446446
}
447447

448-
internal LayoutEvaluatorState? WithDataAccessor(IInstanceDataAccessor dataAccessor)
448+
internal LayoutEvaluatorState WithDataAccessor(IInstanceDataAccessor dataAccessor)
449449
{
450450
return new LayoutEvaluatorState(
451451
dataAccessor,

src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public IInstanceDataAccessor GetPreviousDataAccessor()
115115
throw new NotSupportedException("Legacy single data accessor does not implement GetPreviousDataAccessor");
116116
}
117117

118-
public LayoutEvaluatorState? GetLayoutEvaluatorState()
118+
public LayoutEvaluatorState GetLayoutEvaluatorState()
119119
{
120120
return new LayoutEvaluatorState(
121121
this,

0 commit comments

Comments
 (0)