steam-deck-tools/SteamController/Profiles/Dynamic/RoslynDynamicProfile.cs
Kamil Trzciński e5debff45b SteamController: Use Roslyn Scripting to compile UserProfiles
- This looks into `UserProfiles/` and compiles user profiles
- This exposes a very minimal scripting interface as defined by `Dynamic.Globals`
2023-01-03 11:34:40 +01:00

187 lines
6.5 KiB
C#

using System.Reflection;
using CommonHelpers;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace SteamController.Profiles.Dynamic
{
public sealed partial class RoslynDynamicProfile : Profile
{
private const int ScriptTimeout = 10; // max 10ms
private String fileName;
private Script? compiledScript;
private Profile? inherited;
private DateTime? lastModifiedTime;
private System.Windows.Forms.Timer? watchTimer;
public RoslynDynamicProfile(string name, string fileName, Profile? inherited = null)
{
this.fileName = fileName;
this.inherited = inherited;
this.Name = name;
if (inherited is not null)
this.Name = inherited.Name + ": " + name;
this.Visible = inherited?.Visible ?? true;
this.IsDesktop = inherited?.IsDesktop ?? false;
}
private static ScriptOptions CompilationOptions
{
get
{
var options = ScriptOptions.Default;
// Add Keyboard controls
options = options.AddReferences(typeof(KeyModifiers).Assembly);
options = options.AddImports(typeof(KeyModifiers).FullName);
options = options.AddReferences(typeof(ProfilesSettings.VirtualKeyCode).Assembly);
options = options.AddImports(typeof(ProfilesSettings.VirtualKeyCode).FullName);
return options;
}
}
public bool Compile()
{
this.compiledScript = null;
this.lastModifiedTime = null;
this.Errors = null;
if (!File.Exists(fileName))
{
this.Errors = new string[] { String.Format("File '{0}' does not exist.", fileName) };
return false;
}
try
{
this.lastModifiedTime = File.GetLastWriteTimeUtc(fileName);
using (var file = System.IO.File.OpenRead(fileName))
{
var options = CompilationOptions.WithFilePath(Path.GetFileName(fileName));
var script = CSharpScript.Create(file, options, typeof(Globals));
var compileResult = script.Compile();
var errors = compileResult.Where((result) => result.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error);
if (!errors.Any())
{
this.compiledScript = script;
return true;
}
this.Errors = errors.Select((result) => result.ToString()).ToArray();
OnErrorsChanged();
}
}
catch (Exception e)
{
this.Errors = new string[] { e.Message };
OnErrorsChanged();
}
Log.TraceLine("UserProfile: {0}: Compilation Error", fileName);
foreach (var error in this.Errors)
Log.TraceLine("\t{0}", error);
return false;
}
public void Watch()
{
if (this.lastModifiedTime is null)
return;
if (this.watchTimer is not null)
return;
watchTimer = new System.Windows.Forms.Timer();
watchTimer.Interval = 1000;
watchTimer.Tick += delegate
{
try
{
if (this.lastModifiedTime is null)
return;
var latest = File.GetLastWriteTimeUtc(fileName);
if (this.lastModifiedTime >= latest)
return;
Log.TraceLine("UserProfile: {0}. Detected modification: '{1}' vs '{2}'", fileName, this.lastModifiedTime, latest);
}
catch (Exception) { return; }
Compile();
};
watchTimer.Start();
}
public override bool Selected(Context context)
{
return (this.compiledScript is not null) && (inherited?.Selected(context) ?? true);
}
public override Status Run(Context context)
{
if (inherited?.Run(context).IsDone ?? false)
return Status.Done;
if (this.compiledScript is null)
return Status.Continue;
try
{
var cancelToken = new CancellationTokenSource();
var task = this.compiledScript.RunAsync(new Globals(this, context), cancelToken.Token);
if (!task.Wait(ScriptTimeout))
{
cancelToken.Cancel();
task.Wait();
Log.TraceLine("UserProfile: {0}: Timedout. Canceled.");
}
}
catch (Exception e)
{
Log.TraceLine("UserProfile: {0}: {1}", fileName, e);
}
return Status.Continue;
}
public static IEnumerable<RoslynDynamicProfile> GetUserProfiles(Dictionary<String, Profile> preconfiguredProfiles)
{
var files = new Dictionary<String, String>();
foreach (var directory in GetUserProfilesPaths())
{
foreach (var profile in preconfiguredProfiles)
{
foreach (string file in Directory.GetFiles(directory, profile.Key))
{
String name = Path.GetFileNameWithoutExtension(file);
name = Path.GetFileNameWithoutExtension(name);
yield return new RoslynDynamicProfile(name, file, profile.Value);
}
}
}
}
private static IEnumerable<String> GetUserProfilesPaths()
{
var exePath = Assembly.GetExecutingAssembly().Location;
var exeFolder = Path.GetDirectoryName(exePath);
if (exeFolder is not null)
{
var exeUserProfiles = Path.Combine(exeFolder, "UserProfiles");
if (Directory.Exists(exeUserProfiles))
yield return exeUserProfiles;
}
var documentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var steamControllerDocumentsFolder = Path.Combine(documentsFolder, "SteamController", "UserProfiles");
if (Directory.Exists(steamControllerDocumentsFolder))
yield return steamControllerDocumentsFolder;
}
}
}