5

I'm trying to create a set of C# classes that I can schedule to run at a future time. The intention is to programmatically schedule these via other parts of my code.

This is the current class I'm trying to call via COM.

using System;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.Win32.TaskScheduler;

namespace SchedulableTasks
{
    [Guid("F5CAE94C-BCC7-4304-BEFB-FE1E5D56309A")]
    public class TaskRegistry : ITaskHandler
    {
        private ITaskHandler _taskHandler;
        public void Start(object pHandlerServices, string data)
        {
            var arguments = data.Split('|');
            var taskTypeName = arguments.FirstOrDefault();
            var taskArguments = arguments.Skip(1).FirstOrDefault();
            var taskType = Type.GetType(taskTypeName);
            _taskHandler = (ITaskHandler) Activator.CreateInstance(taskType);
            _taskHandler.Start(pHandlerServices, taskArguments);
        }

        public void Stop(out int pRetCode)
        {
            var retCode = 1;
            _taskHandler?.Stop(out retCode);
            pRetCode = retCode;
        }

        public void Pause()
        {
            _taskHandler.Pause();
        }

        public void Resume()
        {
            _taskHandler.Resume();
        }
    }
}

Here's how I'm attempting to schedule the task

using System;
using Microsoft.Win32.TaskScheduler;
using Newtonsoft.Json;
using Action = Microsoft.Win32.TaskScheduler.Action;

namespace MyProject.Service
{
    public static class SchedulingService
    {
        public enum ScheduledTask
        {
            BillingQuery
        }
        private static readonly Guid RegistryGUI = new Guid("F5CAE94C-BCC7-4304-BEFB-FE1E5D56309A");
        public static void ScheduleAction(string name, string description, Trigger trigger, Action action)
        {
            using (var taskService = new TaskService())
            {
                var task = taskService.NewTask();
                task.RegistrationInfo.Description = description;
                task.Triggers.Add(trigger);
                task.Actions.Add(action);
                taskService.RootFolder.RegisterTaskDefinition(name, task);
            }
        }

        public static Action CreateCSharpAction(ScheduledTask task, object data)
        {
            var taskData = $"{task.ToString()}|{JsonConvert.SerializeObject(data)}";
            return new ComHandlerAction(RegistryGUI, taskData);
        }
    }
}

Microsoft.Win32.TaskScheduler is version 2.7.2 of this library

I can create the scheduled task no problem, but when I attempt to run it, I get a Class not registered (0x80040154).

In my .csproj I'm registering the assembly as a COM object. For the PlatformTarget attribute, I've tried all of AnyCPU, x86, and x64 with the corresponding regasm.exe.

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <RegisterForComInterop>true</RegisterForComInterop>
    <PlatformTarget>x86</PlatformTarget>
  </PropertyGroup>

I'm using the following post-build event to register the .dll.

powershell C:\Windows\Microsoft.Net\Framework64\v4*\RegAsm.exe /codebase '$(ProjectDir)$(OutDir)SchedulableTasks.dll'

As near as I can tell, it's properly registered. Registry Screenshot

Procmon.exe seems to report the same. The thing I'm worried about is the "NAME NOT FOUND" for \TreatAs, \InprocHandler and \InprocServer32\InprocServer32 Procmon Screenshot

Morgan Thrapp
  • 9,748
  • 3
  • 46
  • 67
  • I don't understand what the TaskRegistration class tries to do. It should execute the task, not try to kick the can down the road. Maybe it is an intentional extra indirection, but then the diagnostic is that whatever "BillingQuery" might be is not properly registered. Albeit that this ought not raise this exception since it was already created by Activator.CreateInstance(). Hmm. Just execute the task. – Hans Passant Dec 05 '17 at 01:23
  • @HansPassant The idea was to allow me to register only one class, but have it dispatch to different classes based on the caller. So, I might have one class that calls part of our API, or another one that spins up a server, or sends an email, etc. – Morgan Thrapp Dec 05 '17 at 13:54
  • Okay, by why use ITaskHandler for that?? Why would "BillingQuery" even implement it? You already have the benefit of .NET Reflection and you got as far as creating the object for it. Just use it as-is without demanding that it [ComVisible(true)] and requiring that the task scheduler knows about it. Which it surely doesn't, thus the exception. Just declare a C# interface, give it a Start(string data) method. Implementing Pause and Resume is optional and surely not easy to do in practice. Consider a CancellationToken for Stop. – Hans Passant Dec 05 '17 at 15:10
  • The `ITaskHandler` is unfortunately required by the TaskScheduler library. I agree that it seems like an odd requirement. – Morgan Thrapp Dec 05 '17 at 16:02
  • 1
    It is required, that is what got your TaskRegistry.Start() method to run. It works, got the job done, now it needs to actually execute the task. But it doesn't, it kicks the can and tries to call ITaskHandler::Start() yet again. Nobody around to take care of that one. Note that the TaskRegistry class name is wrong, it should be TaskHandler. It needs to handle the task. – Hans Passant Dec 05 '17 at 16:13

1 Answers1

1

I think I see your problem:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

Try targeting x86. Please see this answer for an in depth description: How to solve COM Exception Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG))?

Edit:

I couldn't repro it, I added a few things:

  1. The Attribute [ComVisible(true)]
  2. A Parameterless Constructor
  3. Signed the Assembly
  4. Had to run the powershell command once manually.

enter image description here

Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321
  • 1
    Nope, even with the `AnyCPU` changed to `x86` I still get the same error. I'd also prefer to register it as an x64 DLL, since the rest of my code is 64 bit. – Morgan Thrapp Dec 04 '17 at 21:37
  • How do I repro this? You've missed the definition of `TaskService`. Do I just create a Class Library (Full.Net) with `TaskRegistry` and then reference it and instantiate it??? – Jeremy Thompson Dec 04 '17 at 21:52
  • `TaskService` comes from [this library](https://github.com/dahall/taskscheduler). Yup, I have the 2 files in their own projects. I have individual task definitions that the `TaskRegistry` references, but anything that inherits from `ITaskHandler` will work. I don't care if it crashes as soon as `TaskRegistry.Start` gets called. Both projects are compiled against 4.6.2. – Morgan Thrapp Dec 04 '17 at 21:57
  • I have referenced David Halls library and am targeting 4.6.1, *did you downvote me?* Kindly provide a [mcve]? – Jeremy Thompson Dec 04 '17 at 22:10
  • I'm still having some issues with the class not being registered. I'll try to come up with a more in-depth MCVE tomorrow. Nope, wasn't me. – Morgan Thrapp Dec 04 '17 at 22:10