2024-06-18 23:44:07 +12:00
using Discord.Commands.Builders ;
using DryIoc ;
using Microsoft.Extensions.DependencyInjection ;
using Ellie.Common.Marmalade ;
using Ellie.Marmalade.Adapters ;
using EllieBot.Common.ModuleBehaviors ;
using System.Collections.Immutable ;
using System.Diagnostics.CodeAnalysis ;
using System.Globalization ;
using System.Reflection ;
using System.Runtime.CompilerServices ;
namespace EllieBot.Marmalade ;
// ReSharper disable RedundantAssignment
public sealed class MarmaladeLoaderService : IMarmaladeLoaderService , IReadyExecutor , IEService
{
private readonly CommandService _cmdService ;
private readonly IBehaviorHandler _behHandler ;
private readonly IPubSub _pubSub ;
private readonly IMarmaladeConfigService _marmaladeConfig ;
private readonly IContainer _cont ;
private readonly ConcurrentDictionary < string , ResolvedMarmalade > _resolved = new ( ) ;
private readonly SemaphoreSlim _lock = new SemaphoreSlim ( 1 , 1 ) ;
private readonly TypedKey < string > _loadKey = new ( "marmalade:load" ) ;
private readonly TypedKey < string > _unloadKey = new ( "marmalade:unload" ) ;
private readonly TypedKey < bool > _stringsReload = new ( "marmalade:reload_strings" ) ;
private const string BASE_DIR = "data/marmalades" ;
public MarmaladeLoaderService (
CommandService cmdService ,
IContainer cont ,
IBehaviorHandler behHandler ,
IPubSub pubSub ,
IMarmaladeConfigService marmaladeConfig )
{
_cmdService = cmdService ;
_behHandler = behHandler ;
_pubSub = pubSub ;
_marmaladeConfig = marmaladeConfig ;
_cont = cont ;
// has to be done this way to support this feature on sharded bots
_pubSub . Sub ( _loadKey , async name = > await InternalLoadAsync ( name ) ) ;
_pubSub . Sub ( _unloadKey , async name = > await InternalUnloadAsync ( name ) ) ;
_pubSub . Sub ( _stringsReload , async _ = > await ReloadStringsInternal ( ) ) ;
}
public IReadOnlyCollection < string > GetAllMarmalades ( )
{
if ( ! Directory . Exists ( BASE_DIR ) )
return Array . Empty < string > ( ) ;
return Directory . GetDirectories ( BASE_DIR )
. Select ( x = > Path . GetRelativePath ( BASE_DIR , x ) )
. ToArray ( ) ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public IReadOnlyCollection < MarmaladeStats > GetLoadedMarmalades ( CultureInfo ? culture )
{
var toReturn = new List < MarmaladeStats > ( _resolved . Count ) ;
foreach ( var ( name , resolvedData ) in _resolved )
{
var canaries = new List < CanaryStats > ( resolvedData . CanaryInfos . Count ) ;
foreach ( var canaryInfos in resolvedData . CanaryInfos . Concat ( resolvedData . CanaryInfos . SelectMany ( x = > x . Subcanaries ) ) )
{
var commands = new List < CanaryCommandStats > ( ) ;
foreach ( var command in canaryInfos . Commands )
{
commands . Add ( new CanaryCommandStats ( command . Aliases . First ( ) ) ) ;
}
canaries . Add ( new CanaryStats ( canaryInfos . Name , canaryInfos . Instance . Prefix , commands ) ) ;
}
toReturn . Add ( new MarmaladeStats ( name , resolvedData . Strings . GetDescription ( culture ) , canaries ) ) ;
}
return toReturn ;
}
public async Task OnReadyAsync ( )
{
foreach ( var name in _marmaladeConfig . GetLoadedMarmalades ( ) )
{
var result = await InternalLoadAsync ( name ) ;
if ( result ! = MarmaladeLoadResult . Success )
Log . Warning ( "Unable to load '{MarmaladeName}' marmalade" , name ) ;
else
Log . Warning ( "Loaded marmalade '{MarmaladeName}'" , name ) ;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public async Task < MarmaladeLoadResult > LoadMarmaladeAsync ( string marmaladeName )
{
// try loading on this shard first to see if it works
var res = await InternalLoadAsync ( marmaladeName ) ;
if ( res = = MarmaladeLoadResult . Success )
{
// if it does publish it so that other shards can load the marmalade too
// this method will be ran twice on this shard but it doesn't matter as
// the second attempt will be ignored
await _pubSub . Pub ( _loadKey , marmaladeName ) ;
}
return res ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public async Task < MarmaladeUnloadResult > UnloadMarmaladeAsync ( string marmaladeName )
{
var res = await InternalUnloadAsync ( marmaladeName ) ;
if ( res = = MarmaladeUnloadResult . Success )
{
await _pubSub . Pub ( _unloadKey , marmaladeName ) ;
}
return res ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public string [ ] GetCommandExampleArgs ( string marmaladeName , string commandName , CultureInfo culture )
{
if ( ! _resolved . TryGetValue ( marmaladeName , out var data ) )
return Array . Empty < string > ( ) ;
return data . Strings . GetCommandStrings ( commandName , culture ) . Args
? ? data . CanaryInfos
. SelectMany ( x = > x . Commands )
. FirstOrDefault ( x = > x . Aliases . Any ( alias
= > alias . Equals ( commandName , StringComparison . InvariantCultureIgnoreCase ) ) )
? . OptionalStrings
. Args
? ? [ string . Empty ] ;
}
public Task ReloadStrings ( )
= > _pubSub . Pub ( _stringsReload , true ) ;
[MethodImpl(MethodImplOptions.NoInlining)]
private void ReloadStringsSync ( )
{
foreach ( var resolved in _resolved . Values )
{
resolved . Strings . Reload ( ) ;
}
}
private async Task ReloadStringsInternal ( )
{
await _lock . WaitAsync ( ) ;
try
{
ReloadStringsSync ( ) ;
}
finally
{
_lock . Release ( ) ;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public string GetCommandDescription ( string marmaladeName , string commandName , CultureInfo culture )
{
if ( ! _resolved . TryGetValue ( marmaladeName , out var data ) )
return string . Empty ;
return data . Strings . GetCommandStrings ( commandName , culture ) . Desc
? ? data . CanaryInfos
. SelectMany ( x = > x . Commands )
. FirstOrDefault ( x = > x . Aliases . Any ( alias
= > alias . Equals ( commandName , StringComparison . InvariantCultureIgnoreCase ) ) )
? . OptionalStrings
. Desc
? ? string . Empty ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async ValueTask < MarmaladeLoadResult > InternalLoadAsync ( string name )
{
if ( _resolved . ContainsKey ( name ) )
return MarmaladeLoadResult . AlreadyLoaded ;
var safeName = Uri . EscapeDataString ( name ) ;
await _lock . WaitAsync ( ) ;
try
{
if ( LoadAssemblyInternal ( safeName ,
out var ctx ,
out var canaryData ,
out var iocModule ,
out var strings ,
out var typeReaders ) )
{
var moduleInfos = new List < ModuleInfo > ( ) ;
LoadTypeReadersInternal ( typeReaders ) ;
foreach ( var point in canaryData )
{
try
{
// initialize canary and subcanaries
await point . Instance . InitializeAsync ( ) ;
foreach ( var sub in point . Subcanaries )
{
await sub . Instance . InitializeAsync ( ) ;
}
var module = await LoadModuleInternalAsync ( name , point , strings , iocModule ) ;
moduleInfos . Add ( module ) ;
}
catch ( Exception ex )
{
Log . Warning ( ex ,
"Error loading canary {CanaryName}" ,
point . Name ) ;
}
}
var execs = GetExecsInternal ( canaryData , strings ) ;
await _behHandler . AddRangeAsync ( execs ) ;
_resolved [ name ] = new ( LoadContext : ctx ,
ModuleInfos : moduleInfos . ToImmutableArray ( ) ,
CanaryInfos : canaryData . ToImmutableArray ( ) ,
strings ,
typeReaders ,
execs )
{
IocModule = iocModule
} ;
_marmaladeConfig . AddLoadedMarmalade ( safeName ) ;
return MarmaladeLoadResult . Success ;
}
return MarmaladeLoadResult . Empty ;
}
catch ( Exception ex ) when ( ex is FileNotFoundException or BadImageFormatException )
{
return MarmaladeLoadResult . NotFound ;
}
catch ( Exception ex )
{
Log . Error ( ex , "An error occurred loading a marmalade" ) ;
return MarmaladeLoadResult . UnknownError ;
}
finally
{
_lock . Release ( ) ;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private IReadOnlyCollection < ICustomBehavior > GetExecsInternal (
IReadOnlyCollection < CanaryInfo > canaryData ,
IMarmaladeStrings strings )
{
var behs = new List < ICustomBehavior > ( ) ;
foreach ( var canary in canaryData )
{
behs . Add ( new BehaviorAdapter ( new ( canary . Instance ) , strings , _cont ) ) ;
foreach ( var sub in canary . Subcanaries )
{
behs . Add ( new BehaviorAdapter ( new ( sub . Instance ) , strings , _cont ) ) ;
}
}
return behs ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void LoadTypeReadersInternal ( Dictionary < Type , TypeReader > typeReaders )
{
var notAddedTypeReaders = new List < Type > ( ) ;
foreach ( var ( type , typeReader ) in typeReaders )
{
// if type reader for this type already exists, it will not be replaced
if ( _cmdService . TypeReaders . Contains ( type ) )
{
notAddedTypeReaders . Add ( type ) ;
continue ;
}
_cmdService . AddTypeReader ( type , typeReader ) ;
}
// remove the ones that were not added
// to prevent them from being unloaded later
// as they didn't come from this marmalade
foreach ( var toRemove in notAddedTypeReaders )
{
typeReaders . Remove ( toRemove ) ;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private bool LoadAssemblyInternal (
string safeName ,
[NotNullWhen(true)] out WeakReference < MarmaladeAssemblyLoadContext > ? ctxWr ,
[NotNullWhen(true)] out IReadOnlyCollection < CanaryInfo > ? canaryData ,
[NotNullWhen(true)] out IIocModule ? iocModule ,
out IMarmaladeStrings strings ,
out Dictionary < Type , TypeReader > typeReaders )
{
ctxWr = null ;
canaryData = null ;
var path = Path . GetFullPath ( $"{BASE_DIR}/{safeName}/{safeName}.dll" ) ;
var dir = Path . GetFullPath ( $"{BASE_DIR}/{safeName}" ) ;
if ( ! Directory . Exists ( dir ) )
throw new DirectoryNotFoundException ( $"Marmalade folder not found: {dir}" ) ;
if ( ! File . Exists ( path ) )
throw new FileNotFoundException ( $"Marmalade dll not found: {path}" ) ;
strings = MarmaladeStrings . CreateDefault ( dir ) ;
2024-09-22 14:24:35 +12:00
var ctx = new MarmaladeAssemblyLoadContext ( path ) ;
2024-06-18 23:44:07 +12:00
var a = ctx . LoadFromAssemblyPath ( Path . GetFullPath ( path ) ) ;
2024-09-22 14:24:35 +12:00
// ctx.LoadDependencies(a);
2024-06-18 23:44:07 +12:00
// load services
iocModule = new MarmaladeNinjectIocModule ( _cont , a , safeName ) ;
iocModule . Load ( ) ;
var sis = LoadCanariesFromAssembly ( safeName , a ) ;
typeReaders = LoadTypeReadersFromAssembly ( a , strings ) ;
if ( sis . Count = = 0 )
{
iocModule . Unload ( ) ;
return false ;
}
ctxWr = new ( ctx ) ;
canaryData = sis ;
return true ;
}
private static readonly Type _paramParserType = typeof ( ParamParser < > ) ;
[MethodImpl(MethodImplOptions.NoInlining)]
private Dictionary < Type , TypeReader > LoadTypeReadersFromAssembly (
Assembly assembly ,
IMarmaladeStrings strings )
{
var paramParsers = assembly . GetExportedTypes ( )
. Where ( x = > x . IsClass
& & ! x . IsAbstract
& & x . BaseType is not null
& & x . BaseType . IsGenericType
& & x . BaseType . GetGenericTypeDefinition ( ) = = _paramParserType ) ;
var typeReaders = new Dictionary < Type , TypeReader > ( ) ;
foreach ( var parserType in paramParsers )
{
var parserObj = ActivatorUtilities . CreateInstance ( _cont , parserType ) ;
var targetType = parserType . BaseType ! . GetGenericArguments ( ) [ 0 ] ;
var typeReaderInstance = ( TypeReader ) Activator . CreateInstance (
typeof ( ParamParserAdapter < > ) . MakeGenericType ( targetType ) ,
args : [ parserObj , strings , _cont ] ) ! ;
typeReaders . Add ( targetType , typeReaderInstance ) ;
}
return typeReaders ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task < ModuleInfo > LoadModuleInternalAsync (
string marmaladeName ,
CanaryInfo canaryInfo ,
IMarmaladeStrings strings ,
IIocModule services )
{
var module = await _cmdService . CreateModuleAsync ( canaryInfo . Instance . Prefix ,
CreateModuleFactory ( marmaladeName , canaryInfo , strings , services ) ) ;
return module ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private Action < ModuleBuilder > CreateModuleFactory (
string marmaladeName ,
CanaryInfo canaryInfo ,
IMarmaladeStrings strings ,
IIocModule iocModule )
= > mb = >
{
var m = mb . WithName ( canaryInfo . Name ) ;
foreach ( var f in canaryInfo . Filters )
{
m . AddPrecondition ( new FilterAdapter ( f , strings ) ) ;
}
foreach ( var cmd in canaryInfo . Commands )
{
m . AddCommand ( cmd . Aliases . First ( ) ,
CreateCallback ( cmd . ContextType ,
new ( canaryInfo ) ,
new ( cmd ) ,
strings ) ,
CreateCommandFactory ( marmaladeName , cmd , strings ) ) ;
}
foreach ( var subInfo in canaryInfo . Subcanaries )
m . AddModule ( subInfo . Instance . Prefix , CreateModuleFactory ( marmaladeName , subInfo , strings , iocModule ) ) ;
} ;
private static readonly RequireContextAttribute _reqGuild = new RequireContextAttribute ( ContextType . Guild ) ;
private static readonly RequireContextAttribute _reqDm = new RequireContextAttribute ( ContextType . DM ) ;
private Action < CommandBuilder > CreateCommandFactory ( string marmaladeName , CanaryCommandData cmd , IMarmaladeStrings strings )
= > ( cb ) = >
{
cb . AddAliases ( cmd . Aliases . Skip ( 1 ) . ToArray ( ) ) ;
if ( cmd . ContextType = = CommandContextType . Guild )
cb . AddPrecondition ( _reqGuild ) ;
else if ( cmd . ContextType = = CommandContextType . Dm )
cb . AddPrecondition ( _reqDm ) ;
foreach ( var f in cmd . Filters )
cb . AddPrecondition ( new FilterAdapter ( f , strings ) ) ;
foreach ( var ubp in cmd . UserAndBotPerms )
{
if ( ubp is user_permAttribute up )
{
if ( up . GuildPerm is { } gp )
cb . AddPrecondition ( new UserPermAttribute ( gp ) ) ;
else if ( up . ChannelPerm is { } cp )
cb . AddPrecondition ( new UserPermAttribute ( cp ) ) ;
}
else if ( ubp is bot_permAttribute bp )
{
if ( bp . GuildPerm is { } gp )
cb . AddPrecondition ( new BotPermAttribute ( gp ) ) ;
else if ( bp . ChannelPerm is { } cp )
cb . AddPrecondition ( new BotPermAttribute ( cp ) ) ;
}
else if ( ubp is bot_owner_onlyAttribute )
{
cb . AddPrecondition ( new OwnerOnlyAttribute ( ) ) ;
}
}
cb . WithPriority ( cmd . Priority ) ;
// using summary to save method name
// method name is used to retrieve desc/usages
cb . WithRemarks ( $"marmalade///{marmaladeName}" ) ;
cb . WithSummary ( cmd . MethodInfo . Name . ToLowerInvariant ( ) ) ;
foreach ( var param in cmd . Parameters )
{
cb . AddParameter ( param . Name , param . Type , CreateParamFactory ( param ) ) ;
}
} ;
private Action < ParameterBuilder > CreateParamFactory ( ParamData paramData )
= > ( pb ) = >
{
pb . WithIsMultiple ( paramData . IsParams )
. WithIsOptional ( paramData . IsOptional )
. WithIsRemainder ( paramData . IsLeftover ) ;
if ( paramData . IsOptional )
pb . WithDefault ( paramData . DefaultValue ) ;
} ;
[MethodImpl(MethodImplOptions.NoInlining)]
private Func < ICommandContext , object [ ] , IServiceProvider , CommandInfo , Task > CreateCallback (
CommandContextType contextType ,
WeakReference < CanaryInfo > canaryDataWr ,
WeakReference < CanaryCommandData > canaryCommandDataWr ,
IMarmaladeStrings strings )
= > async (
context ,
parameters ,
svcs ,
_ ) = >
{
if ( ! canaryCommandDataWr . TryGetTarget ( out var cmdData )
| | ! canaryDataWr . TryGetTarget ( out var canaryData ) )
{
Log . Warning ( "Attempted to run an unloaded canary's command" ) ;
return ;
}
var paramObjs = ParamObjs ( contextType , cmdData , parameters , context , svcs , _cont , strings ) ;
try
{
var methodInfo = cmdData . MethodInfo ;
if ( methodInfo . ReturnType = = typeof ( Task )
| | ( methodInfo . ReturnType . IsGenericType
& & methodInfo . ReturnType . GetGenericTypeDefinition ( ) = = typeof ( Task < > ) ) )
{
await ( Task ) methodInfo . Invoke ( canaryData . Instance , paramObjs ) ! ;
}
else if ( methodInfo . ReturnType = = typeof ( ValueTask ) )
{
await ( ( ValueTask ) methodInfo . Invoke ( canaryData . Instance , paramObjs ) ! ) . AsTask ( ) ;
}
else // if (methodInfo.ReturnType == typeof(void))
{
methodInfo . Invoke ( canaryData . Instance , paramObjs ) ;
}
}
finally
{
paramObjs = null ;
cmdData = null ;
canaryData = null ;
}
} ;
[MethodImpl(MethodImplOptions.NoInlining)]
private static object [ ] ParamObjs (
CommandContextType contextType ,
CanaryCommandData cmdData ,
object [ ] parameters ,
ICommandContext context ,
IServiceProvider svcs ,
IServiceProvider svcProvider ,
IMarmaladeStrings strings )
{
var extraParams = contextType = = CommandContextType . Unspecified ? 0 : 1 ;
extraParams + = cmdData . InjectedParams . Count ;
var paramObjs = new object [ parameters . Length + extraParams ] ;
var startAt = 0 ;
if ( contextType ! = CommandContextType . Unspecified )
{
paramObjs [ 0 ] = ContextAdapterFactory . CreateNew ( context , strings , svcs ) ;
startAt = 1 ;
}
for ( var i = 0 ; i < cmdData . InjectedParams . Count ; i + + )
{
var svc = svcProvider . GetService ( cmdData . InjectedParams [ i ] ) ;
if ( svc is null )
{
throw new ArgumentException ( $"Cannot inject a service of type {cmdData.InjectedParams[i]}" ) ;
}
paramObjs [ i + startAt ] = svc ;
svc = null ;
}
startAt + = cmdData . InjectedParams . Count ;
for ( var i = 0 ; i < parameters . Length ; i + + )
paramObjs [ startAt + i ] = parameters [ i ] ;
return paramObjs ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task < MarmaladeUnloadResult > InternalUnloadAsync ( string name )
{
if ( ! _resolved . Remove ( name , out var lsi ) )
return MarmaladeUnloadResult . NotLoaded ;
await _lock . WaitAsync ( ) ;
try
{
UnloadTypeReaders ( lsi . TypeReaders ) ;
foreach ( var mi in lsi . ModuleInfos )
{
await _cmdService . RemoveModuleAsync ( mi ) ;
}
await _behHandler . RemoveRangeAsync ( lsi . Execs ) ;
await DisposeCanaryInstances ( lsi ) ;
var lc = lsi . LoadContext ;
var km = lsi . IocModule ;
lsi . IocModule . Unload ( ) ;
lsi . IocModule = null ! ;
if ( km is IDisposable d )
d . Dispose ( ) ;
lsi = null ;
_marmaladeConfig . RemoveLoadedMarmalade ( name ) ;
return UnloadInternal ( lc )
? MarmaladeUnloadResult . Success
: MarmaladeUnloadResult . PossiblyUnable ;
}
finally
{
_lock . Release ( ) ;
}
}
private void UnloadTypeReaders ( Dictionary < Type , TypeReader > valueTypeReaders )
{
foreach ( var tr in valueTypeReaders )
{
_cmdService . TryRemoveTypeReader ( tr . Key , false , out _ ) ;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task DisposeCanaryInstances ( ResolvedMarmalade marmalade )
{
foreach ( var si in marmalade . CanaryInfos )
{
try
{
await si . Instance . DisposeAsync ( ) ;
foreach ( var sub in si . Subcanaries )
{
await sub . Instance . DisposeAsync ( ) ;
}
}
catch ( Exception ex )
{
Log . Warning ( ex ,
"Failed cleanup of Canary {CanaryName}. This marmalade might not unload correctly" ,
si . Instance . Name ) ;
}
}
// marmalades = null;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private bool UnloadInternal ( WeakReference < MarmaladeAssemblyLoadContext > lsi )
{
UnloadContext ( lsi ) ;
GcCleanup ( ) ;
return ! lsi . TryGetTarget ( out _ ) ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void UnloadContext ( WeakReference < MarmaladeAssemblyLoadContext > lsiLoadContext )
{
if ( lsiLoadContext . TryGetTarget ( out var ctx ) )
{
ctx . Unload ( ) ;
}
}
private void GcCleanup ( )
{
// cleanup
for ( var i = 0 ; i < 10 ; i + + )
{
GC . Collect ( ) ;
GC . WaitForPendingFinalizers ( ) ;
GC . WaitForFullGCComplete ( ) ;
GC . Collect ( ) ;
}
}
private static readonly Type _canaryType = typeof ( Canary ) ;
// [MethodImpl(MethodImplOptions.NoInlining)]
// private MarmaladeIoCKernelModule LoadMarmaladeServicesInternal(string name, Assembly a)
// => new MarmaladeIoCKernelModule(name, a);
[MethodImpl(MethodImplOptions.NoInlining)]
public IReadOnlyCollection < CanaryInfo > LoadCanariesFromAssembly ( string name , Assembly a )
{
// find all types in teh assembly
var types = a . GetExportedTypes ( ) ;
// canary is always a public non abstract class
var classes = types . Where ( static x = > x . IsClass
& & ( x . IsNestedPublic | | x . IsPublic )
& & ! x . IsAbstract
& & x . BaseType = = _canaryType
& & ( x . DeclaringType is null | | x . DeclaringType . IsAssignableTo ( _canaryType ) ) )
. ToList ( ) ;
var topModules = new Dictionary < Type , CanaryInfo > ( ) ;
foreach ( var cl in classes )
{
if ( cl . DeclaringType is not null )
continue ;
// get module data, and add it to the topModules dictionary
var module = GetModuleData ( cl ) ;
topModules . Add ( cl , module ) ;
}
foreach ( var c in classes )
{
if ( c . DeclaringType is not Type dt )
continue ;
// if there is no top level module which this module is a child of
// just print a warning and skip it
if ( ! topModules . TryGetValue ( dt , out var parentData ) )
{
Log . Warning ( "Can't load submodule {SubName} because parent module {Name} does not exist" ,
c . Name ,
dt . Name ) ;
continue ;
}
GetModuleData ( c , parentData ) ;
}
return topModules . Values . ToArray ( ) ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private CanaryInfo GetModuleData ( Type type , CanaryInfo ? parentData = null )
{
var filters = type . GetCustomAttributes < FilterAttribute > ( true )
. ToArray ( ) ;
var instance = ( Canary ) ActivatorUtilities . CreateInstance ( _cont , type ) ;
var module = new CanaryInfo ( instance . Name ,
parentData ,
instance ,
GetCommands ( instance , type ) ,
filters ) ;
if ( parentData is not null )
parentData . Subcanaries . Add ( module ) ;
return module ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private IReadOnlyCollection < CanaryCommandData > GetCommands ( Canary instance , Type type )
{
var methodInfos = type
. GetMethods ( BindingFlags . Instance
| BindingFlags . DeclaredOnly
| BindingFlags . Public )
. Where ( static x = >
{
if ( x . GetCustomAttribute < cmdAttribute > ( true ) is null )
return false ;
if ( x . ReturnType . IsGenericType )
{
var genericType = x . ReturnType . GetGenericTypeDefinition ( ) ;
if ( genericType = = typeof ( Task < > ) )
return true ;
// if (genericType == typeof(ValueTask<>))
// return true;
Log . Warning ( "Method {MethodName} has an invalid return type: {ReturnType}" ,
x . Name ,
x . ReturnType ) ;
return false ;
}
var succ = x . ReturnType = = typeof ( Task )
| | x . ReturnType = = typeof ( ValueTask )
| | x . ReturnType = = typeof ( void ) ;
if ( ! succ )
{
Log . Warning ( "Method {MethodName} has an invalid return type: {ReturnType}" ,
x . Name ,
x . ReturnType ) ;
}
return succ ;
} ) ;
var cmds = new List < CanaryCommandData > ( ) ;
foreach ( var method in methodInfos )
{
var filters = method . GetCustomAttributes < FilterAttribute > ( true ) . ToArray ( ) ;
var userAndBotPerms = method . GetCustomAttributes < MarmaladePermAttribute > ( true )
. ToArray ( ) ;
var prio = method . GetCustomAttribute < prioAttribute > ( true ) ? . Priority ? ? 0 ;
var paramInfos = method . GetParameters ( ) ;
var cmdParams = new List < ParamData > ( ) ;
var diParams = new List < Type > ( ) ;
var cmdContext = CommandContextType . Unspecified ;
var canInject = false ;
for ( var paramCounter = 0 ; paramCounter < paramInfos . Length ; paramCounter + + )
{
var pi = paramInfos [ paramCounter ] ;
var paramName = pi . Name ? ? "unnamed" ;
var isContext = paramCounter = = 0 & & pi . ParameterType . IsAssignableTo ( typeof ( AnyContext ) ) ;
var leftoverAttribute = pi . GetCustomAttribute < leftoverAttribute > ( true ) ;
var hasDefaultValue = pi . HasDefaultValue ;
var defaultValue = pi . DefaultValue ;
var isLeftover = leftoverAttribute ! = null ;
var isParams = pi . GetCustomAttribute < ParamArrayAttribute > ( ) is not null ;
var paramType = pi . ParameterType ;
var isInjected = pi . GetCustomAttribute < injectAttribute > ( true ) is not null ;
if ( isContext )
{
if ( hasDefaultValue | | leftoverAttribute ! = null | | isParams )
throw new ArgumentException (
"IContext parameter cannot be optional, leftover, constant or params. "
+ GetErrorPath ( method , pi ) ) ;
if ( paramCounter ! = 0 )
throw new ArgumentException ( $"IContext parameter has to be first. {GetErrorPath(method, pi)}" ) ;
canInject = true ;
if ( paramType . IsAssignableTo ( typeof ( GuildContext ) ) )
cmdContext = CommandContextType . Guild ;
else if ( paramType . IsAssignableTo ( typeof ( DmContext ) ) )
cmdContext = CommandContextType . Dm ;
else
cmdContext = CommandContextType . Any ;
continue ;
}
if ( isInjected )
{
if ( ! canInject & & paramCounter ! = 0 )
throw new ArgumentException ( $"Parameters marked as [Injected] have to come after IContext" ) ;
canInject = true ;
diParams . Add ( paramType ) ;
continue ;
}
canInject = false ;
if ( isParams )
{
if ( hasDefaultValue )
throw new NotSupportedException ( "Params can't have const values at the moment. "
+ GetErrorPath ( method , pi ) ) ;
// if it's params, it means it's an array, and i only need a parser for the actual type,
// as the parser will run on each array element, it can't be null
paramType = paramType . GetElementType ( ) ! ;
}
// leftover can only be the last parameter.
if ( isLeftover & & paramCounter ! = paramInfos . Length - 1 )
{
var path = GetErrorPath ( method , pi ) ;
Log . Error ( "Only one parameter can be marked [Leftover] and it has to be the last one. {Path} " ,
path ) ;
throw new ArgumentException ( "Leftover attribute error." ) ;
}
cmdParams . Add ( new ParamData ( paramType , paramName , hasDefaultValue , defaultValue , isLeftover , isParams ) ) ;
}
var cmdAttribute = method . GetCustomAttribute < cmdAttribute > ( true ) ! ;
var aliases = cmdAttribute . Aliases ;
if ( aliases . Length = = 0 )
aliases = [ method . Name . ToLowerInvariant ( ) ] ;
cmds . Add ( new (
aliases ,
method ,
instance ,
filters ,
userAndBotPerms ,
cmdContext ,
diParams ,
cmdParams ,
new ( cmdAttribute . desc , cmdAttribute . args ) ,
prio
) ) ;
}
return cmds ;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private string GetErrorPath ( MethodInfo m , System . Reflection . ParameterInfo pi )
= > $ @ "Module: {m.DeclaringType?.Name}
Command : { m . Name }
ParamName : { pi . Name }
ParamType : { pi . ParameterType . Name } ";
}