Skip to content

Commit

Permalink
Ensure worker process is executable in linux consumption (Azure#9550)
Browse files Browse the repository at this point in the history
  • Loading branch information
jviau authored Sep 21, 2023
1 parent 62222b9 commit 0adf5a6
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 35 deletions.
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
- Update WebJobsScriptHostService to remove hardcoded sleep during application shut down (#9520)
- Update PowerShell 7.2 Worker Version to [4.0.2974](https://github.com/Azure/azure-functions-powershell-worker/releases/tag/v4.0.2974)
- Update PowerShell 7.0 Worker Version to [4.0.2973](https://github.com/Azure/azure-functions-powershell-worker/releases/tag/v4.0.2973)
- Add support for standalone executable (ie: `dotnet build --standalone`) for out-of-proc workers in Linux Consumption.
30 changes: 2 additions & 28 deletions src/WebJobs.Script/Workers/Http/HttpWorkerProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Eventing;
using Microsoft.Extensions.Logging;
using Mono.Unix;

namespace Microsoft.Azure.WebJobs.Script.Workers.Http
{
Expand All @@ -19,7 +18,6 @@ internal class HttpWorkerProcess : WorkerProcess
private readonly string _scriptRootPath;
private readonly string _workerId;
private readonly WorkerProcessArguments _workerProcessArguments;
private readonly IEnvironment _environment;

internal HttpWorkerProcess(string workerId,
string rootScriptPath,
Expand All @@ -33,7 +31,7 @@ internal HttpWorkerProcess(string workerId,
IMetricsLogger metricsLogger,
IServiceProvider serviceProvider,
ILoggerFactory loggerFactory)
: base(eventManager, processRegistry, workerProcessLogger, consoleLogSource, metricsLogger, serviceProvider, loggerFactory, httpWorkerOptions.Description.UseStdErrorStreamForErrorsOnly)
: base(eventManager, processRegistry, workerProcessLogger, consoleLogSource, metricsLogger, serviceProvider, loggerFactory, environment, httpWorkerOptions.Description.UseStdErrorStreamForErrorsOnly)
{
_processFactory = processFactory;
_eventManager = eventManager;
Expand All @@ -42,7 +40,6 @@ internal HttpWorkerProcess(string workerId,
_scriptRootPath = rootScriptPath;
_httpWorkerOptions = httpWorkerOptions;
_workerProcessArguments = _httpWorkerOptions.Arguments;
_environment = environment;
}

internal override Process CreateWorkerProcess()
Expand All @@ -60,31 +57,8 @@ internal override Process CreateWorkerProcess()
workerContext.EnvironmentVariables.Add(HttpWorkerConstants.CustomHandlerPortEnvVarName, _httpWorkerOptions.Port.ToString());
workerContext.EnvironmentVariables.Add(HttpWorkerConstants.CustomHandlerWorkerIdEnvVarName, _workerId);
workerContext.EnvironmentVariables.Add(HttpWorkerConstants.FunctionAppRootVarName, _scriptRootPath);
Process workerProcess = _processFactory.CreateWorkerProcess(workerContext);
if (_environment.IsAnyLinuxConsumption())
{
AssignUserExecutePermissionsIfNotExists(workerProcess.StartInfo.FileName);
}
return workerProcess;
}

private void AssignUserExecutePermissionsIfNotExists(string filePath)
{
try
{
UnixFileInfo fileInfo = new UnixFileInfo(filePath);
if (!fileInfo.FileAccessPermissions.HasFlag(FileAccessPermissions.UserExecute))
{
_workerProcessLogger.LogDebug("Assigning execute permissions to file: {filePath}", filePath);
fileInfo.FileAccessPermissions |= FileAccessPermissions.UserExecute |
FileAccessPermissions.GroupExecute |
FileAccessPermissions.OtherExecute;
}
}
catch (Exception ex)
{
_workerProcessLogger.LogWarning(ex, "Error while assigning execute permission.");
}
return _processFactory.CreateWorkerProcess(workerContext);
}

internal override void HandleWorkerProcessExitError(WorkerProcessExitException httpWorkerProcessExitException)
Expand Down
35 changes: 34 additions & 1 deletion src/WebJobs.Script/Workers/ProcessManagement/WorkerProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Azure.WebJobs.Script.Eventing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Mono.Unix;

namespace Microsoft.Azure.WebJobs.Script.Workers
{
Expand All @@ -27,13 +28,14 @@ internal abstract class WorkerProcess : IWorkerProcess, IDisposable
private readonly IServiceProvider _serviceProvider;
private readonly IDisposable _eventSubscription;
private readonly Lazy<ILogger> _toolingConsoleJsonLoggerLazy;
private readonly IEnvironment _environment;

private bool _useStdErrorStreamForErrorsOnly;
private Queue<string> _processStdErrDataQueue = new Queue<string>(3);
private IHostProcessMonitor _processMonitor;
private object _syncLock = new object();

internal WorkerProcess(IScriptEventManager eventManager, IProcessRegistry processRegistry, ILogger workerProcessLogger, IWorkerConsoleLogSource consoleLogSource, IMetricsLogger metricsLogger, IServiceProvider serviceProvider, ILoggerFactory loggerFactory, bool useStdErrStreamForErrorsOnly = false)
internal WorkerProcess(IScriptEventManager eventManager, IProcessRegistry processRegistry, ILogger workerProcessLogger, IWorkerConsoleLogSource consoleLogSource, IMetricsLogger metricsLogger, IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IEnvironment environment, bool useStdErrStreamForErrorsOnly = false)
{
_processRegistry = processRegistry;
_workerProcessLogger = workerProcessLogger;
Expand All @@ -42,6 +44,7 @@ internal WorkerProcess(IScriptEventManager eventManager, IProcessRegistry proces
_metricsLogger = metricsLogger;
_useStdErrorStreamForErrorsOnly = useStdErrStreamForErrorsOnly;
_serviceProvider = serviceProvider;
_environment = environment;
_toolingConsoleJsonLoggerLazy = new Lazy<ILogger>(() => loggerFactory.CreateLogger(WorkerConstants.ToolingConsoleLogCategoryName), isThreadSafe: true);

// We subscribe to host start events so we can handle the restart that occurs
Expand All @@ -66,6 +69,11 @@ public Task StartProcessAsync()
using (_metricsLogger.LatencyEvent(MetricEventNames.ProcessStart))
{
Process = CreateWorkerProcess();
if (_environment.IsAnyLinuxConsumption())
{
AssignUserExecutePermissionsIfNotExists();
}

try
{
Process.ErrorDataReceived += (sender, e) => OnErrorDataReceived(sender, e);
Expand Down Expand Up @@ -305,5 +313,30 @@ internal void UnregisterFromProcessMonitor()
}
}
}

private void AssignUserExecutePermissionsIfNotExists()
{
try
{
if (Process is not { } p)
{
return;
}

string filePath = p.StartInfo.FileName;
UnixFileInfo fileInfo = new UnixFileInfo(filePath);
if (!fileInfo.FileAccessPermissions.HasFlag(FileAccessPermissions.UserExecute))
{
_workerProcessLogger.LogDebug("Assigning execute permissions to file: {filePath}", filePath);
fileInfo.FileAccessPermissions |= FileAccessPermissions.UserExecute |
FileAccessPermissions.GroupExecute |
FileAccessPermissions.OtherExecute;
}
}
catch (Exception ex)
{
_workerProcessLogger.LogWarning(ex, "Error while assigning execute permission.");
}
}
}
}
2 changes: 1 addition & 1 deletion src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal RpcWorkerProcess(string runtime,
IOptions<FunctionsHostingConfigOptions> hostingConfigOptions,
IEnvironment environment,
ILoggerFactory loggerFactory)
: base(eventManager, processRegistry, workerProcessLogger, consoleLogSource, metricsLogger, serviceProvider, loggerFactory, rpcWorkerConfig.Description.UseStdErrorStreamForErrorsOnly)
: base(eventManager, processRegistry, workerProcessLogger, consoleLogSource, metricsLogger, serviceProvider, loggerFactory, environment, rpcWorkerConfig.Description.UseStdErrorStreamForErrorsOnly)
{
_runtime = runtime;
_processFactory = processFactory;
Expand Down
14 changes: 12 additions & 2 deletions test/WebJobs.Script.Tests/Workers/Http/HttpWorkerProcessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Config;
using Microsoft.Azure.WebJobs.Script.Eventing;
using Microsoft.Azure.WebJobs.Script.Workers;
Expand Down Expand Up @@ -66,12 +67,21 @@ public void CreateWorkerProcess_VerifyEnvVars(string processEnvValue)
}

[Fact]
public void CreateWorkerProcess_LinuxConsumption_AssingnsExecutePermissions_invoked()
public async Task StartProcess_LinuxConsumption_TriesToAssignExecutePermissions()
{
TestEnvironment testEnvironment = new TestEnvironment();
testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerName, "TestContainer");
var mockHttpWorkerProcess = new HttpWorkerProcess(_testWorkerId, _rootScriptPath, _httpWorkerOptions, _mockEventManager.Object, _defaultWorkerProcessFactory, _processRegistry, _testLogger, _languageWorkerConsoleLogSource.Object, testEnvironment, new TestMetricsLogger(), _serviceProviderMock.Object, new LoggerFactory());
mockHttpWorkerProcess.CreateWorkerProcess();

try
{
await mockHttpWorkerProcess.StartProcessAsync();
}
catch
{
// expected to throw. Just verifying a log statement occured before then.
}

// Verify method invocation
var testLogs = _testLogger.GetLogMessages();
Assert.Contains("Error while assigning execute permission", testLogs[0].FormattedMessage);
Expand Down
4 changes: 2 additions & 2 deletions test/WebJobs.Script.Tests/Workers/TestWorkerProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Workers
{
internal class TestWorkerProcess : WorkerProcess
{
internal TestWorkerProcess(IScriptEventManager eventManager, IProcessRegistry processRegistry, ILogger workerProcessLogger, IWorkerConsoleLogSource consoleLogSource, IMetricsLogger metricsLogger, IServiceProvider serviceProvider, ILoggerFactory loggerFactory, bool useStdErrStreamForErrorsOnly = false)
: base(eventManager, processRegistry, workerProcessLogger, consoleLogSource, metricsLogger, serviceProvider, loggerFactory, useStdErrStreamForErrorsOnly)
internal TestWorkerProcess(IScriptEventManager eventManager, IProcessRegistry processRegistry, ILogger workerProcessLogger, IWorkerConsoleLogSource consoleLogSource, IMetricsLogger metricsLogger, IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IEnvironment environment, bool useStdErrStreamForErrorsOnly = false)
: base(eventManager, processRegistry, workerProcessLogger, consoleLogSource, metricsLogger, serviceProvider, loggerFactory, environment, useStdErrStreamForErrorsOnly)
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task WorkerConsoleLogService_ConsoleLogs_LogLevel_Expected(bool use
_workerConsoleLogService = new WorkerConsoleLogService(_testUserLogger, _workerConsoleLogSource);
_serviceProviderMock = new Mock<IServiceProvider>(MockBehavior.Strict);

WorkerProcess workerProcess = new TestWorkerProcess(_eventManager, _processRegistry, _testSystemLogger, _workerConsoleLogSource, null, _serviceProviderMock.Object, _testLoggerFactory, useStdErrForErrorLogsOnly);
WorkerProcess workerProcess = new TestWorkerProcess(_eventManager, _processRegistry, _testSystemLogger, _workerConsoleLogSource, null, _serviceProviderMock.Object, _testLoggerFactory, Mock.Of<IEnvironment>(), useStdErrForErrorLogsOnly);
workerProcess.ParseErrorMessageAndLog("Test Message No keyword");
workerProcess.ParseErrorMessageAndLog("Test Error Message");
workerProcess.ParseErrorMessageAndLog("Test Warning Message");
Expand Down

0 comments on commit 0adf5a6

Please sign in to comment.