2023-06-02 21:47:59 +02:00
using System ;
2020-01-19 10:32:13 -05:00
using System.IO ;
using GitHub.DistributedTask.Expressions2.Sdk ;
using GitHub.DistributedTask.Pipelines.ContextData ;
using GitHub.DistributedTask.Pipelines.ObjectTemplating ;
using GitHub.Runner.Sdk ;
using System.Reflection ;
using System.Threading ;
using System.Collections.Generic ;
2022-02-14 15:06:08 +01:00
using GitHub.Runner.Common.Util ;
2020-01-19 10:32:13 -05:00
2020-03-18 12:08:51 -04:00
namespace GitHub.Runner.Worker.Expressions
2020-01-19 10:32:13 -05:00
{
2020-03-18 12:08:51 -04:00
public sealed class HashFilesFunction : Function
2020-01-19 10:32:13 -05:00
{
2020-06-05 23:02:10 -04:00
private const int _hashFileTimeoutSeconds = 120 ;
2020-01-19 10:32:13 -05:00
protected sealed override Object EvaluateCore (
EvaluationContext context ,
out ResultMemory resultMemory )
{
resultMemory = null ;
var templateContext = context . State as DistributedTask . ObjectTemplating . TemplateContext ;
ArgUtil . NotNull ( templateContext , nameof ( templateContext ));
templateContext . ExpressionValues . TryGetValue ( PipelineTemplateConstants . GitHub , out var githubContextData );
ArgUtil . NotNull ( githubContextData , nameof ( githubContextData ));
var githubContext = githubContextData as DictionaryContextData ;
ArgUtil . NotNull ( githubContext , nameof ( githubContext ));
2023-06-29 12:12:23 +02:00
if (! githubContext . TryGetValue ( PipelineTemplateConstants . HostWorkspace , out var workspace ))
{
githubContext . TryGetValue ( PipelineTemplateConstants . Workspace , out workspace );
}
ArgUtil . NotNull ( workspace , nameof ( workspace ));
2020-01-19 10:32:13 -05:00
var workspaceData = workspace as StringContextData ;
ArgUtil . NotNull ( workspaceData , nameof ( workspaceData ));
string githubWorkspace = workspaceData . Value ;
2023-06-29 12:12:23 +02:00
2020-01-19 10:32:13 -05:00
bool followSymlink = false ;
2022-10-18 10:54:08 -04:00
List < string > patterns = new ();
2020-01-19 10:32:13 -05:00
var firstParameter = true ;
foreach ( var parameter in Parameters )
{
var parameterString = parameter . Evaluate ( context ). ConvertToString ();
if ( firstParameter )
{
firstParameter = false ;
if ( parameterString . StartsWith ( "--" ))
{
if ( string . Equals ( parameterString , "--follow-symbolic-links" , StringComparison . OrdinalIgnoreCase ))
{
followSymlink = true ;
continue ;
}
else
{
throw new ArgumentOutOfRangeException ( $"Invalid glob option {parameterString}, avaliable option: '--follow-symbolic-links'." );
}
}
}
patterns . Add ( parameterString );
}
context . Trace . Info ( $"Search root directory: '{githubWorkspace}'" );
context . Trace . Info ( $"Search pattern: '{string.Join(" , ", patterns)}'" );
string binDir = Path . GetDirectoryName ( Assembly . GetEntryAssembly (). Location );
string runnerRoot = new DirectoryInfo ( binDir ). Parent . FullName ;
2022-02-25 20:59:02 +01:00
string node = Path . Combine ( runnerRoot , "externals" , NodeUtil . GetInternalNodeVersion (), "bin" , $"node{IOUtil.ExeExtension}" );
2020-01-19 10:32:13 -05:00
string hashFilesScript = Path . Combine ( binDir , "hashFiles" );
var hashResult = string . Empty ;
2020-03-18 12:08:51 -04:00
var p = new ProcessInvoker ( new HashFilesTrace ( context . Trace ));
2020-01-19 10:32:13 -05:00
p . ErrorDataReceived += (( _ , data ) =>
{
if (! string . IsNullOrEmpty ( data . Data ) && data . Data . StartsWith ( "__OUTPUT__" ) && data . Data . EndsWith ( "__OUTPUT__" ))
{
hashResult = data . Data . Substring ( 10 , data . Data . Length - 20 );
context . Trace . Info ( $"Hash result: '{hashResult}'" );
}
else
{
context . Trace . Info ( data . Data );
}
});
p . OutputDataReceived += (( _ , data ) =>
{
context . Trace . Info ( data . Data );
});
var env = new Dictionary < string , string >();
if ( followSymlink )
{
env [ "followSymbolicLinks" ] = "true" ;
}
env [ "patterns" ] = string . Join ( Environment . NewLine , patterns );
2020-06-05 23:02:10 -04:00
using ( var tokenSource = new CancellationTokenSource ( TimeSpan . FromSeconds ( _hashFileTimeoutSeconds )))
2020-01-19 10:32:13 -05:00
{
2020-06-05 23:02:10 -04:00
try
{
int exitCode = p . ExecuteAsync ( workingDirectory : githubWorkspace ,
fileName : node ,
arguments : $"\" { hashFilesScript . Replace ( "\"" , "\\\"" )} \ "" ,
environment : env ,
requireExitCodeZero : false ,
cancellationToken : tokenSource . Token ). GetAwaiter (). GetResult ();
2020-01-19 10:32:13 -05:00
2020-06-05 23:02:10 -04:00
if ( exitCode != 0 )
{
throw new InvalidOperationException ( $"hashFiles('{ExpressionUtility.StringEscape(string.Join(" , ", patterns))}') failed. Fail to hash files under directory '{githubWorkspace}'" );
}
}
catch ( OperationCanceledException ) when ( tokenSource . IsCancellationRequested )
{
throw new TimeoutException ( $"hashFiles('{ExpressionUtility.StringEscape(string.Join(" , ", patterns))}') couldn't finish within {_hashFileTimeoutSeconds} seconds." );
}
return hashResult ;
}
2020-01-19 10:32:13 -05:00
}
2020-03-18 12:08:51 -04:00
private sealed class HashFilesTrace : ITraceWriter
{
private GitHub . DistributedTask . Expressions2 . ITraceWriter _trace ;
public HashFilesTrace ( GitHub . DistributedTask . Expressions2 . ITraceWriter trace )
{
_trace = trace ;
}
public void Info ( string message )
{
_trace . Info ( message );
}
public void Verbose ( string message )
{
_trace . Info ( message );
}
}
2020-01-19 10:32:13 -05:00
}
2025-11-07 14:18:52 -06:00
public sealed class NewHashFilesFunction : GitHub . Actions . Expressions . Sdk . Function
{
private const int _hashFileTimeoutSeconds = 120 ;
protected sealed override Object EvaluateCore (
GitHub . Actions . Expressions . Sdk . EvaluationContext context ,
out GitHub . Actions . Expressions . Sdk . ResultMemory resultMemory )
{
resultMemory = null ;
var templateContext = context . State as GitHub . Actions . WorkflowParser . ObjectTemplating . TemplateContext ;
ArgUtil . NotNull ( templateContext , nameof ( templateContext ));
templateContext . ExpressionValues . TryGetValue ( PipelineTemplateConstants . GitHub , out var githubContextData );
ArgUtil . NotNull ( githubContextData , nameof ( githubContextData ));
var githubContext = githubContextData as GitHub . Actions . Expressions . Data . DictionaryExpressionData ;
ArgUtil . NotNull ( githubContext , nameof ( githubContext ));
if (! githubContext . TryGetValue ( PipelineTemplateConstants . HostWorkspace , out var workspace ))
{
githubContext . TryGetValue ( PipelineTemplateConstants . Workspace , out workspace );
}
ArgUtil . NotNull ( workspace , nameof ( workspace ));
var workspaceData = workspace as GitHub . Actions . Expressions . Data . StringExpressionData ;
ArgUtil . NotNull ( workspaceData , nameof ( workspaceData ));
string githubWorkspace = workspaceData . Value ;
bool followSymlink = false ;
List < string > patterns = new ();
var firstParameter = true ;
foreach ( var parameter in Parameters )
{
var parameterString = parameter . Evaluate ( context ). ConvertToString ();
if ( firstParameter )
{
firstParameter = false ;
if ( parameterString . StartsWith ( "--" ))
{
if ( string . Equals ( parameterString , "--follow-symbolic-links" , StringComparison . OrdinalIgnoreCase ))
{
followSymlink = true ;
continue ;
}
else
{
throw new ArgumentOutOfRangeException ( $"Invalid glob option {parameterString}, avaliable option: '--follow-symbolic-links'." );
}
}
}
patterns . Add ( parameterString );
}
context . Trace . Info ( $"Search root directory: '{githubWorkspace}'" );
context . Trace . Info ( $"Search pattern: '{string.Join(" , ", patterns)}'" );
string binDir = Path . GetDirectoryName ( Assembly . GetEntryAssembly (). Location );
string runnerRoot = new DirectoryInfo ( binDir ). Parent . FullName ;
string node = Path . Combine ( runnerRoot , "externals" , NodeUtil . GetInternalNodeVersion (), "bin" , $"node{IOUtil.ExeExtension}" );
string hashFilesScript = Path . Combine ( binDir , "hashFiles" );
var hashResult = string . Empty ;
var p = new ProcessInvoker ( new NewHashFilesTrace ( context . Trace ));
p . ErrorDataReceived += (( _ , data ) =>
{
if (! string . IsNullOrEmpty ( data . Data ) && data . Data . StartsWith ( "__OUTPUT__" ) && data . Data . EndsWith ( "__OUTPUT__" ))
{
hashResult = data . Data . Substring ( 10 , data . Data . Length - 20 );
context . Trace . Info ( $"Hash result: '{hashResult}'" );
}
else
{
context . Trace . Info ( data . Data );
}
});
p . OutputDataReceived += (( _ , data ) =>
{
context . Trace . Info ( data . Data );
});
var env = new Dictionary < string , string >();
if ( followSymlink )
{
env [ "followSymbolicLinks" ] = "true" ;
}
env [ "patterns" ] = string . Join ( Environment . NewLine , patterns );
using ( var tokenSource = new CancellationTokenSource ( TimeSpan . FromSeconds ( _hashFileTimeoutSeconds )))
{
try
{
int exitCode = p . ExecuteAsync ( workingDirectory : githubWorkspace ,
fileName : node ,
arguments : $"\" { hashFilesScript . Replace ( "\"" , "\\\"" )} \ "" ,
environment : env ,
requireExitCodeZero : false ,
cancellationToken : tokenSource . Token ). GetAwaiter (). GetResult ();
if ( exitCode != 0 )
{
throw new InvalidOperationException ( $"hashFiles('{ExpressionUtility.StringEscape(string.Join(" , ", patterns))}') failed. Fail to hash files under directory '{githubWorkspace}'" );
}
}
catch ( OperationCanceledException ) when ( tokenSource . IsCancellationRequested )
{
throw new TimeoutException ( $"hashFiles('{ExpressionUtility.StringEscape(string.Join(" , ", patterns))}') couldn't finish within {_hashFileTimeoutSeconds} seconds." );
}
return hashResult ;
}
}
private sealed class NewHashFilesTrace : ITraceWriter
{
private GitHub . Actions . Expressions . ITraceWriter _trace ;
public NewHashFilesTrace ( GitHub . Actions . Expressions . ITraceWriter trace )
{
_trace = trace ;
}
public void Info ( string message )
{
_trace . Info ( message );
}
public void Verbose ( string message )
{
_trace . Info ( message );
}
}
}
2023-06-02 21:47:59 +02:00
}