Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ protected override async IAsyncEnumerable<UserInterfaceRepositoryActionBase> Map
var arguments = await action.Arguments.RenderAsync(context).ConfigureAwait(false);

yield return new UserInterfaceRepositoryAction(name, repository)
{
RepositoryCommand = new StartProcessRepositoryCommand(command, arguments),
};
{
RepositoryCommand = new StartProcessRepositoryCommand(command, arguments),
};
}
}
71 changes: 61 additions & 10 deletions src/RepoM.Api/IO/ProcessHelper.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,85 @@
namespace RepoM.Api.IO;

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Extensions.Logging;
using static RepoM.Api.IO.ProcessHelper;

public static class ProcessHelper
{
public static void StartProcess(string process, string arguments, ILogger logger)
{
List<Exception> exceptionList = [];

var psi = new ProcessStartInfo()
{
FileName = process, // The file to run.
Arguments = arguments, // Arguments to pass to the process.
UseShellExecute = false, // Execute the process directly rather than using the shell. Similar to double-clicking the file in Explorer or the RunAs command.
};

try
{
Process.Start(process, arguments);
var processState = Process.Start(psi);

// This is probably never null as RunAs always throws an exception if there is a problem, but we check just in case.
if (null == processState) { throw new Exception("Process failed to start."); }

logger.LogInformation("Successfully started process {Process} with arguments {Arguments} via primary method.", process, arguments);
return;
}
catch (Exception)
catch (Exception ex)
{
// swallow, retry below.
/*
* This method wil fail if the process is:
* a. not found in the PATH system variable
* b. not found in %LOCALAPPDATA%\Microsoft\WindowsApps
* c. %LOCALAPPDATA%\Microsoft\WindowsApps is not in the PATH.
*/
logger.LogInformation(ex, "Failed to start process {Process} with arguments {Arguments} via primary method. Attempt via secondary method.", process, arguments);
exceptionList.Add(ex);
}

try
{
var psi = new ProcessStartInfo(process, arguments)
{
UseShellExecute = true,
};
Process.Start(psi);
psi.UseShellExecute = true; // Uses shell execute to allow for file associations to work. The default is true on .NET Framework apps and false on .NET Core apps.
psi.WindowStyle = ProcessWindowStyle.Hidden; // Hides the shell/terminal window.
var processState = Process.Start(psi);

// Sometimes the shell execute method fails to start the process but does not throw an exception so we need to check if the process is null.
if (null == processState) { throw new Exception("Process failed to start."); }

logger.LogInformation("Successfully started process {Process} with arguments {Arguments} via secondary method.", process, arguments);
return;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to start process {Process} with arguments {Arguments}", process, arguments);
logger.LogInformation(ex, "Failed to start process {Process} with arguments {Arguments} via secondary method.", process, arguments);
exceptionList.Add(ex);
}

/*
* We only log the exceptions as errors if we failed to start the process through all the methods.
* Otherwise, we log them as information.
*/
foreach ((Exception currentException, var index) in exceptionList.WithIndex())
{
logger.LogError(currentException, "Failed to start process {Process} with arguments {Arguments}. [{Index}/{Count}]", process, arguments, index, exceptionList.Count);
}

}
}

}


// [CODE REFACTORING] Move this class somewhere else

// ReSharper disable once InconsistentNaming
public static class IEnumerableExtensions
{
public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self)
=> self?.Select((item, index) => (item, index)) ?? new List<(T, int)>();
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ public StartProcessCommandExecutor(ILogger logger)
_logger = logger;
}

/*
* The purpose of this function is to execute a process specified by the
* Executable property of the repositoryCommand object, along with
* any additional arguments passed in the Arguments property.
* The ProcessHelper.StartProcess method is responsible for starting
* the process with the specified executable and arguments.
* Overall, this function takes care of executing a process based on
* the provided repository command, handling different scenarios
* for the number of arguments,
* and logging any relevant information using the _logger object.
*/
public void Execute(IRepository repository, StartProcessRepositoryCommand repositoryCommand)
{
var args = string.Empty;
Expand Down
18 changes: 10 additions & 8 deletions src/RepoM.Api/Resources/RepositoryActionsV2.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
context:
context:
- type: evaluate-script@1
content: |-
func is_null(input)
Expand Down Expand Up @@ -90,9 +90,10 @@ context:

action-menu:

- type: command@1
name: Open in Windows File Explorer
command: '"{{ repository.path }}"'
- type : command@1
name : Open in Windows File Explorer
command : explorer
arguments : '{{ repository.path }}'

- type: command@1
name: Open in Windows Terminal
Expand Down Expand Up @@ -123,10 +124,11 @@ action-menu:
name: '{{ get_filename(sln) }}'
command: '{{ sln }}'

- type: executable@1
name: Open in Visual Studio Code
executable: '{{ exe_vs_code }}'
arguments: '"{{ repository.linux_path }}"'
- type : command@1
name : Open in Visual Studio Code
command : '{{ exe_vs_code }}'
arguments : '"{{ repository.path }}"'
active : file.file_exists(exe_vs_code)

- type: executable@1
name: Open in Sourcetree
Expand Down