├── .gitignore ├── Install.bat ├── README.md ├── Uninstall.bat └── src ├── TaskScheduler.sln └── TaskScheduler ├── App.config ├── App_Data └── installHangfireDatabase.sql ├── Core ├── ActionRunner.cs ├── JobsManager.cs ├── LogEverythingAttribute.cs └── LogFailureAttribute.cs ├── Helper ├── AssemblyInfo.cs ├── DatabaseHelper.cs ├── FileManager.cs ├── HttpHelper.cs ├── NotificationHelper.cs └── StringHelper.cs ├── Jobs ├── Delayed.cs ├── FireAndForget.cs ├── IJob.cs ├── Job.cs ├── JobType.cs ├── JobsSetting.cs ├── Recurring.cs └── SettingWrapper.cs ├── NLog.config ├── NLog.xsd ├── NotificationServices ├── CallRestApi │ └── CallRestApiService.cs ├── Email │ ├── EmailService.cs │ └── EmailTemplate.html ├── INotificationService.cs ├── NotificaionType.cs ├── Notification.cs ├── NotificationService.cs ├── NotifyCondition.cs ├── SMS │ ├── CellphoneNumber.cs │ ├── RahyabSmsService.cs │ └── SmsService.cs ├── Slack │ └── SlackService.cs ├── Status.cs ├── SystemNotification.cs └── Telegram │ └── TelegramService.cs ├── Program.cs ├── Properties ├── AssemblyInfo.cs ├── Settings.Designer.cs └── Settings.settings ├── Service.cs ├── Startup.cs ├── TaskScheduler.csproj ├── TaskSchedulerSetting.json ├── app.manifest └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | !packages/*/build/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | *_i.c 26 | *_p.c 27 | *.ilk 28 | *.meta 29 | *.obj 30 | *.pch 31 | *.pdb 32 | *.pgc 33 | *.pgd 34 | *.rsp 35 | *.sbr 36 | *.tlb 37 | *.tli 38 | *.tlh 39 | *.tmp 40 | *.tmp_proj 41 | *.log 42 | *.vspscc 43 | *.vssscc 44 | .builds 45 | *.pidb 46 | *.log 47 | *.scc 48 | !Infrastructure/Hasin.Taaghche.Infrastructure.Log 49 | 50 | # Visual C++ cache files 51 | ipch/ 52 | *.aps 53 | *.ncb 54 | *.opensdf 55 | *.sdf 56 | *.cachefile 57 | 58 | # Visual Studio profiler 59 | *.psess 60 | *.vsp 61 | *.vspx 62 | 63 | # Guidance Automation Toolkit 64 | *.gpState 65 | 66 | # ReSharper is a .NET coding add-in 67 | _ReSharper*/ 68 | *.[Rr]e[Ss]harper 69 | 70 | # TeamCity is a build add-in 71 | _TeamCity* 72 | 73 | # DotCover is a Code Coverage Tool 74 | *.dotCover 75 | 76 | # NCrunch 77 | *.ncrunch* 78 | .*crunch*.local.xml 79 | 80 | # Installshield output folder 81 | [Ee]xpress/ 82 | 83 | # DocProject is a documentation generator add-in 84 | DocProject/buildhelp/ 85 | DocProject/Help/*.HxT 86 | DocProject/Help/*.HxC 87 | DocProject/Help/*.hhc 88 | DocProject/Help/*.hhk 89 | DocProject/Help/*.hhp 90 | DocProject/Help/Html2 91 | DocProject/Help/html 92 | 93 | # Click-Once directory 94 | publish/ 95 | 96 | # Publish Web Output 97 | *.Publish.xml 98 | 99 | # NuGet Packages Directory 100 | packages/ 101 | 102 | # Windows Azure Build Output 103 | csx 104 | *.build.csdef 105 | 106 | # Windows Store app package directory 107 | AppPackages/ 108 | 109 | # Others 110 | sql/ 111 | *.Cache 112 | ClientBin/ 113 | [Ss]tyle[Cc]op.* 114 | ~$* 115 | *~ 116 | *.dbmdl 117 | *.[Pp]ublish.xml 118 | *.pfx 119 | *.publishsettings 120 | 121 | # RIA/Silverlight projects 122 | Generated_Code/ 123 | 124 | # Backup & report files from converting an old project file to a newer 125 | # Visual Studio version. Backup files are not needed, because we have git ;-) 126 | _UpgradeReport_Files/ 127 | Backup*/ 128 | UpgradeLog*.XML 129 | UpgradeLog*.htm 130 | 131 | # SQL Server files 132 | App_Data/*.mdf 133 | App_Data/*.ldf 134 | 135 | 136 | #LightSwitch generated files 137 | GeneratedArtifacts/ 138 | _Pvt_Extensions/ 139 | ModelManifest.xml 140 | 141 | # ========================= 142 | # Windows detritus 143 | # ========================= 144 | 145 | # Windows image file caches 146 | Thumbs.db 147 | ehthumbs.db 148 | 149 | # Folder config file 150 | Desktop.ini 151 | 152 | # Recycle Bin used on file shares 153 | $RECYCLE.BIN/ 154 | 155 | # Mac desktop service store files 156 | .DS_Store 157 | 158 | # NuGet package files 159 | *.nupkg 160 | 161 | .vs -------------------------------------------------------------------------------- /Install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | "%cd%\src\TaskScheduler\bin\Output\TaskScheduler.exe" install start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task Scheduler 2 | 3 | Dynamic Task Scheduler as windows service. 4 | 5 | This service can send your notifications at a special time or according by resluts of jobs conditions. 6 | You must provide `Jobs`, `Notifications`, `NotificationServices` in the setting file to execute this application. 7 | The setting file's checked every `100 seconds` to set new jobs or remove some services if that changed. So you should wait for 2 minute atleast to see your changed. The application worked based on [Hangfire](https://www.hangfire.io/) and SQL Server. 8 | 9 | ### System Requirments for open this project 10 | 11 | * Visual Studio 2015 (run as admin) 12 | * .NET Framework 4.6 13 | * SQL Server 2008 R2 or higher versions 14 | * Windows 7 or higher versions 15 | 16 | -------------- 17 | 18 | ## How to work 19 | 20 | For install Task Scheduler service on windows, run CMD as administrator and execute the below codes: 21 | 22 | `$ TaskScheduler.exe install start` 23 | 24 | or run `Install.bat` file in from root folder. 25 | Now you can use the application and enjoy it; 26 | 27 | For Uninstall app: 28 | 29 | ```bash 30 | $ sc stop TaskScheduler 31 | $ sc delete TaskScheduler 32 | ``` 33 | 34 | or run `Uninstall.bat` file in from root folder. 35 | 36 | -------------- 37 | 38 | ## How to registering for your WebHook Url with Slack 39 | 40 | [Click here to set up an incoming WebHook](https://my.slack.com/services/new/incoming-webhook/) 41 | 42 | * Sign in to Slack. 43 | * Choose a channel to post to. 44 | * Then click on the green button `Add Incoming WebHooks integration`. 45 | * You will be given a WebHook Url. Keep this private. Use it when you set up the Slack Notification Service. 46 | 47 | -------------- 48 | 49 | ## How to use or create new Telegram notification service 50 | 51 | In the `notificationServices` setting you can add your own services like telegram service, for example: 52 | 53 | ```json 54 | "notificationServices": [ 55 | //... 56 | { 57 | "serviceName": "testTelegram", // is notification service name which called from notifications 58 | "notificationType": "TelegramService", // declare service action type and is must important to have correct type. 59 | "username": "@NameOfBot", 60 | "apiKey": "684394290:AAFg3Ht1EtNB7iYe9V_VVxKCEVMP-lSjycA", 61 | "senderBot": "@NameOfBot", 62 | "isDefaultService": true // can be used for system notifications 63 | } 64 | //... 65 | ] 66 | ``` 67 | 68 | * First [create a **Telegram Bot**](https://core.telegram.org/bots#creating-a-new-bot) from [BotFather](https://telegram.me/botfather). 69 | 70 | * Get the Bot API token from BotFather. The token is a string along the lines of `110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw` that is required to authorize the bot and send requests to the Bot API. 71 | 72 | * Set the `apiKey` property of service by api token and set the bot name in `senderBot` property; 73 | 74 | Add your bot as administrator user to channel or group which you want to send message or notification (This is not available in Telegram Desktop version); 75 | 76 | Enter channel or group name like @testChannel in the notification receiver. for example: 77 | 78 | ```json 79 | "notifications": [ 80 | //... 81 | { 82 | "notificationServiceName": "testTelegram", 83 | "receiver": "123456789, @testChannel" 84 | } 85 | //... 86 | ], 87 | ``` 88 | 89 | -------------- 90 | 91 | ## How to add jobs and notification services to setting file 92 | 93 | Please change the `TaskSchedulerSetting.json` file as well as for what you want to execute and notifying. 94 | This file have 3 part: 95 | 96 | * list of your jobs by types: 97 | * __FireAndForget__ (this jobs are executed only once and almost immediatelyafter creation) 98 | * __Delayed__ (this jobs are executed only once too, but not immediately, after a certain time interval) 99 | * __Recurring__ (this jobs fire many times on the specified schedule) 100 | * list of default notifications receivers 101 | * list of notification services by type: 102 | * __Email__ (this service provide send mail from your special mail service data) 103 | * __Sms__ (this is a Short Message Service provider) 104 | * __Telegram__ (this service is for send notifications from a telegram bot to a channel or group which the bot is administrator user member of that) 105 | * __Slack__ (this service provide a webhook url to send notifications to your slack channels) 106 | * __CallRestApi__ (is a provider to call or send notifications for your Restfull APIs, must times used for monitoring the APIs) 107 | 108 | For example of the below setting, you can see that slack notification service defined in `notificationServices` part and by type `SlackService` and name `testSlack` which called from `notifications` part for service provider. 109 | 110 | And in `notifications` part used `SlackService` for `alerts` channel as notification receiver. This part can define notifications to all jobs as deault receiver, but if a job have self notification definition, so just will send notify to that. 111 | 112 | ```json 113 | 114 | { 115 | "jobs": [ 116 | { 117 | "jobType": "Recurring", 118 | "enable": true, 119 | "name": "max-purchese checker (per hour)", 120 | "description": "Call purchese web api every hour for max purchase", 121 | "actionName": "CallRestApi", 122 | "actionParameters": { 123 | "url": "http://localhost:8002/v1/monitor/highpayment", 124 | "httpMethod": "GET", 125 | "queryParameters": { 126 | "duration": 60, 127 | "maxTotal": 200, 128 | "maxPerUser": 20 129 | } 130 | }, 131 | "notifyCondition": "NotEquals", 132 | "notifyConditionResult": "OK: \"\"", 133 | "triggerOn": "0 * * * *", // every hour 134 | "notifications": [ 135 | { 136 | "notificationServiceName": "testSlack", 137 | "receiver": "alerts;" 138 | } 139 | ] 140 | }, 141 | { 142 | "jobType": "Recurring", 143 | "enable": true, 144 | "name": "User Account Creates Checker", 145 | "description": "Check users account create count per minutes", 146 | "actionName": "CallRestApi", 147 | "actionParameters": { 148 | "url": "http://localhost:8002/v1/monitor/account", 149 | "queryParameters": { 150 | "duration": 60, 151 | "min": 0 152 | }, 153 | "httpMethod": "GET" 154 | }, 155 | "notifyCondition": "NotEquals", 156 | "notifyConditionResult": "OK: \"\"", 157 | "triggerOn": "0 */2 * * *" // every 2 hours 158 | }, 159 | { 160 | "jobType": "Recurring", 161 | "enable": true, 162 | "name": "log checker", 163 | "description": "Remove more than 7 days ago files", 164 | "actionName": "StartProgram", 165 | "actionParameters": { 166 | "fileName": "forfiles", 167 | "arguments": "/p \"C:\\inetpub\\logs\\LogFiles\" /s /m *.* /c \"cmd /c Del @path\" /d -7", 168 | "windowsStyle": "Hidden", 169 | "createNoWindow": true, 170 | "useShellExecute": false 171 | }, 172 | "triggerOn": "0 12 * * 5" // At 12:00 PM, Only on Friday 173 | } 174 | ], 175 | "notifications": [ 176 | { 177 | "notificationServiceName": "RahyabSmsService", 178 | "receiver": "09354028149;09123456789;" 179 | }, 180 | { 181 | "notificationServiceName": "taaghcheMailService", 182 | "receiver": "bezzad@gmail.com;" 183 | }, 184 | { 185 | "notificationServiceName": "testSlack", 186 | "receiver": "alerts" 187 | }, 188 | { 189 | "notificationServiceName": "testTelegram", 190 | "receiver": "106752126, @telester" 191 | } 192 | ], 193 | "notificationServices": [ 194 | { 195 | "serviceName": "RahyabSmsService", 196 | "notificationType": "SmsService", 197 | "username": "1000", 198 | "password": "TakTak", 199 | "senderNumber": "50001760", 200 | "isDefaultService": true 201 | }, 202 | { 203 | "serviceName": "taaghcheMailService", 204 | "notificationType": "EmailService", 205 | "smtpPassword": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 206 | "smtpSenderEmail": "postmaster@test.com", 207 | "smtpUrl": "smtp.mailgun.org", 208 | "smtpPort": 587, 209 | "logo": null, 210 | "logoUrl": "https://a.slack-edge.com/9c217/img/loading_hash_animation.gif", 211 | "clickUrl": "https://test.com", 212 | "unsubscribeUrl": "http://api.test.com/unsubscribe/?mail={receiver}", 213 | "isDefaultService": true 214 | }, 215 | { 216 | "serviceName": "testTelegram", 217 | "notificationType": "TelegramService", 218 | "username": "@telesterbot", 219 | "apiKey": "684394290:AAFg3Ht1EtNB7iYe9V_VVxKCEVMP-lSjycA", 220 | "senderBot": "@telesterbot", 221 | "isDefaultService": true 222 | }, 223 | { 224 | "serviceName": "testSlack", 225 | "notificationType": "SlackService", 226 | "senderName": "TaskScheduler {#version}", 227 | "webhookUrl": "https://hooks.slack.com/services/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", 228 | "iconUrl": ":robot_face:", 229 | "isDefaultService": false 230 | }, 231 | { 232 | "serviceName": "CallRestApi", 233 | "notificationType": "CallRestApiService", 234 | "clientId": "api", 235 | "clientSecret": "XXXXXXXXXXXXXXXXXXXX", 236 | "authServerUrl": "https://auth.test.com/connect/token", 237 | "authGrantType": "client_credentials", 238 | "authScope": "service", 239 | "isDefaultService": true 240 | } 241 | ] 242 | } 243 | 244 | ``` 245 | -------------------------------------------------------------------------------- /Uninstall.bat: -------------------------------------------------------------------------------- 1 | 2 | @echo off 3 | 4 | :: BatchGotAdmin 5 | :------------------------------------- 6 | REM --> Check for permissions 7 | >nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system" 8 | 9 | REM --> If error flag set, we do not have admin. 10 | if '%errorlevel%' NEQ '0' ( 11 | echo Requesting administrative privileges... 12 | goto UACPrompt 13 | ) else ( goto gotAdmin ) 14 | 15 | :UACPrompt 16 | echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs" 17 | set params = %*:"="" 18 | echo UAC.ShellExecute "cmd.exe", "/c %~s0 %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" 19 | 20 | "%temp%\getadmin.vbs" 21 | del "%temp%\getadmin.vbs" 22 | exit /B 23 | 24 | :gotAdmin 25 | pushd "%CD%" 26 | CD /D "%~dp0" 27 | :-------------------------------------- 28 | 29 | sc stop "TaskScheduler" 30 | sc delete "TaskScheduler" 31 | pause -------------------------------------------------------------------------------- /src/TaskScheduler.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskScheduler", "TaskScheduler\TaskScheduler.csproj", "{A0A1BFE1-D843-45DB-8884-7C0EA1351670}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A0A1BFE1-D843-45DB-8884-7C0EA1351670}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A0A1BFE1-D843-45DB-8884-7C0EA1351670}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A0A1BFE1-D843-45DB-8884-7C0EA1351670}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A0A1BFE1-D843-45DB-8884-7C0EA1351670}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {56D36DF9-67CF-4B57-9D2E-E667BCDA711B} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/TaskScheduler/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | TaskSchedulerSetting.json 38 | 39 | 40 | App_Data.installHangfireDatabase.sql 41 | 42 | 43 | 8002 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/TaskScheduler/App_Data/installHangfireDatabase.sql: -------------------------------------------------------------------------------- 1 | 2 | -- This file is part of Hangfire. 3 | -- Copyright © 2013-2014 Sergey Odinokov. 4 | -- 5 | -- Hangfire is free software: you can redistribute it and/or modify 6 | -- it under the terms of the GNU Lesser General Public License as 7 | -- published by the Free Software Foundation, either version 3 8 | -- of the License, or any later version. 9 | -- 10 | -- Hangfire is distributed in the hope that it will be useful, 11 | -- but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | -- GNU Lesser General Public License for more details. 14 | -- 15 | -- You should have received a copy of the GNU Lesser General Public 16 | -- License along with Hangfire. If not, see . 17 | 18 | SET NOCOUNT ON 19 | DECLARE @TARGET_SCHEMA_VERSION INT; 20 | SET @TARGET_SCHEMA_VERSION = 5; 21 | 22 | PRINT 'Installing Hangfire SQL objects...'; 23 | 24 | BEGIN TRANSACTION; 25 | BEGIN TRY; 26 | 27 | -- Acquire exclusive lock to prevent deadlocks caused by schema creation / version update 28 | DECLARE @SchemaLockResult INT; 29 | EXEC @SchemaLockResult = sp_getapplock @Resource = 'HangFire:SchemaLock', @LockMode = 'Exclusive' 30 | 31 | -- Create the database schema if it doesn't exists 32 | IF NOT EXISTS (SELECT [schema_id] FROM [sys].[schemas] WHERE [name] = 'HangFire') 33 | BEGIN 34 | EXEC (N'CREATE SCHEMA [HangFire]'); 35 | PRINT 'Created database schema [HangFire]'; 36 | END 37 | ELSE 38 | PRINT 'Database schema [HangFire] already exists'; 39 | 40 | DECLARE @SCHEMA_ID int; 41 | SELECT @SCHEMA_ID = [schema_id] FROM [sys].[schemas] WHERE [name] = 'HangFire'; 42 | 43 | -- Create the [HangFire].Schema table if not exists 44 | IF NOT EXISTS(SELECT [object_id] FROM [sys].[tables] 45 | WHERE [name] = 'Schema' AND [schema_id] = @SCHEMA_ID) 46 | BEGIN 47 | CREATE TABLE [HangFire].[Schema]( 48 | [Version] [int] NOT NULL, 49 | CONSTRAINT [PK_HangFire_Schema] PRIMARY KEY CLUSTERED ([Version] ASC) 50 | ); 51 | PRINT 'Created table [HangFire].[Schema]'; 52 | END 53 | ELSE 54 | PRINT 'Table [HangFire].[Schema] already exists'; 55 | 56 | DECLARE @CURRENT_SCHEMA_VERSION int; 57 | SELECT @CURRENT_SCHEMA_VERSION = [Version] FROM [HangFire].[Schema]; 58 | 59 | PRINT 'Current Hangfire schema version: ' + CASE @CURRENT_SCHEMA_VERSION WHEN NULL THEN 'none' ELSE CONVERT(nvarchar, @CURRENT_SCHEMA_VERSION) END; 60 | 61 | IF @CURRENT_SCHEMA_VERSION IS NOT NULL AND @CURRENT_SCHEMA_VERSION > @TARGET_SCHEMA_VERSION 62 | BEGIN 63 | ROLLBACK TRANSACTION; 64 | RAISERROR(N'Hangfire current database schema version %d is newer than the configured SqlServerStorage schema version %d. Please update to the latest Hangfire.SqlServer NuGet package.', 11, 1, 65 | @CURRENT_SCHEMA_VERSION, @TARGET_SCHEMA_VERSION); 66 | END 67 | 68 | -- Install [HangFire] schema objects 69 | IF @CURRENT_SCHEMA_VERSION IS NULL 70 | BEGIN 71 | PRINT 'Installing schema version 1'; 72 | 73 | -- Create job tables 74 | CREATE TABLE [HangFire].[Job] ( 75 | [Id] [int] IDENTITY(1,1) NOT NULL, 76 | [StateId] [int] NULL, 77 | [StateName] [nvarchar](20) NULL, -- To speed-up queries. 78 | [InvocationData] [nvarchar](max) NOT NULL, 79 | [Arguments] [nvarchar](max) NOT NULL, 80 | [CreatedAt] [datetime] NOT NULL, 81 | [ExpireAt] [datetime] NULL, 82 | 83 | CONSTRAINT [PK_HangFire_Job] PRIMARY KEY CLUSTERED ([Id] ASC) 84 | ); 85 | PRINT 'Created table [HangFire].[Job]'; 86 | 87 | CREATE NONCLUSTERED INDEX [IX_HangFire_Job_StateName] ON [HangFire].[Job] ([StateName] ASC); 88 | PRINT 'Created index [IX_HangFire_Job_StateName]'; 89 | 90 | -- Job history table 91 | 92 | CREATE TABLE [HangFire].[State] ( 93 | [Id] [int] IDENTITY(1,1) NOT NULL, 94 | [JobId] [int] NOT NULL, 95 | [Name] [nvarchar](20) NOT NULL, 96 | [Reason] [nvarchar](100) NULL, 97 | [CreatedAt] [datetime] NOT NULL, 98 | [Data] [nvarchar](max) NULL, 99 | 100 | CONSTRAINT [PK_HangFire_State] PRIMARY KEY CLUSTERED ([Id] ASC) 101 | ); 102 | PRINT 'Created table [HangFire].[State]'; 103 | 104 | ALTER TABLE [HangFire].[State] ADD CONSTRAINT [FK_HangFire_State_Job] FOREIGN KEY([JobId]) 105 | REFERENCES [HangFire].[Job] ([Id]) 106 | ON UPDATE CASCADE 107 | ON DELETE CASCADE; 108 | PRINT 'Created constraint [FK_HangFire_State_Job]'; 109 | 110 | CREATE NONCLUSTERED INDEX [IX_HangFire_State_JobId] ON [HangFire].[State] ([JobId] ASC); 111 | PRINT 'Created index [IX_HangFire_State_JobId]'; 112 | 113 | -- Job parameters table 114 | 115 | CREATE TABLE [HangFire].[JobParameter]( 116 | [Id] [int] IDENTITY(1,1) NOT NULL, 117 | [JobId] [int] NOT NULL, 118 | [Name] [nvarchar](40) NOT NULL, 119 | [Value] [nvarchar](max) NULL, 120 | 121 | CONSTRAINT [PK_HangFire_JobParameter] PRIMARY KEY CLUSTERED ([Id] ASC) 122 | ); 123 | PRINT 'Created table [HangFire].[JobParameter]'; 124 | 125 | ALTER TABLE [HangFire].[JobParameter] ADD CONSTRAINT [FK_HangFire_JobParameter_Job] FOREIGN KEY([JobId]) 126 | REFERENCES [HangFire].[Job] ([Id]) 127 | ON UPDATE CASCADE 128 | ON DELETE CASCADE; 129 | PRINT 'Created constraint [FK_HangFire_JobParameter_Job]'; 130 | 131 | CREATE NONCLUSTERED INDEX [IX_HangFire_JobParameter_JobIdAndName] ON [HangFire].[JobParameter] ( 132 | [JobId] ASC, 133 | [Name] ASC 134 | ); 135 | PRINT 'Created index [IX_HangFire_JobParameter_JobIdAndName]'; 136 | 137 | -- Job queue table 138 | 139 | CREATE TABLE [HangFire].[JobQueue]( 140 | [Id] [int] IDENTITY(1,1) NOT NULL, 141 | [JobId] [int] NOT NULL, 142 | [Queue] [nvarchar](20) NOT NULL, 143 | [FetchedAt] [datetime] NULL, 144 | 145 | CONSTRAINT [PK_HangFire_JobQueue] PRIMARY KEY CLUSTERED ([Id] ASC) 146 | ); 147 | PRINT 'Created table [HangFire].[JobQueue]'; 148 | 149 | CREATE NONCLUSTERED INDEX [IX_HangFire_JobQueue_JobIdAndQueue] ON [HangFire].[JobQueue] ( 150 | [JobId] ASC, 151 | [Queue] ASC 152 | ); 153 | PRINT 'Created index [IX_HangFire_JobQueue_JobIdAndQueue]'; 154 | 155 | CREATE NONCLUSTERED INDEX [IX_HangFire_JobQueue_QueueAndFetchedAt] ON [HangFire].[JobQueue] ( 156 | [Queue] ASC, 157 | [FetchedAt] ASC 158 | ); 159 | PRINT 'Created index [IX_HangFire_JobQueue_QueueAndFetchedAt]'; 160 | 161 | -- Servers table 162 | 163 | CREATE TABLE [HangFire].[Server]( 164 | [Id] [nvarchar](50) NOT NULL, 165 | [Data] [nvarchar](max) NULL, 166 | [LastHeartbeat] [datetime] NULL, 167 | 168 | CONSTRAINT [PK_HangFire_Server] PRIMARY KEY CLUSTERED ([Id] ASC) 169 | ); 170 | PRINT 'Created table [HangFire].[Server]'; 171 | 172 | -- Extension tables 173 | 174 | CREATE TABLE [HangFire].[Hash]( 175 | [Id] [int] IDENTITY(1,1) NOT NULL, 176 | [Key] [nvarchar](100) NOT NULL, 177 | [Name] [nvarchar](40) NOT NULL, 178 | [StringValue] [nvarchar](max) NULL, 179 | [IntValue] [int] NULL, 180 | [ExpireAt] [datetime] NULL, 181 | 182 | CONSTRAINT [PK_HangFire_Hash] PRIMARY KEY CLUSTERED ([Id] ASC) 183 | ); 184 | PRINT 'Created table [HangFire].[Hash]'; 185 | 186 | CREATE UNIQUE NONCLUSTERED INDEX [UX_HangFire_Hash_KeyAndName] ON [HangFire].[Hash] ( 187 | [Key] ASC, 188 | [Name] ASC 189 | ); 190 | PRINT 'Created index [UX_HangFire_Hash_KeyAndName]'; 191 | 192 | CREATE TABLE [HangFire].[List]( 193 | [Id] [int] IDENTITY(1,1) NOT NULL, 194 | [Key] [nvarchar](100) NOT NULL, 195 | [Value] [nvarchar](max) NULL, 196 | [ExpireAt] [datetime] NULL, 197 | 198 | CONSTRAINT [PK_HangFire_List] PRIMARY KEY CLUSTERED ([Id] ASC) 199 | ); 200 | PRINT 'Created table [HangFire].[List]'; 201 | 202 | CREATE TABLE [HangFire].[Set]( 203 | [Id] [int] IDENTITY(1,1) NOT NULL, 204 | [Key] [nvarchar](100) NOT NULL, 205 | [Score] [float] NOT NULL, 206 | [Value] [nvarchar](256) NOT NULL, 207 | [ExpireAt] [datetime] NULL, 208 | 209 | CONSTRAINT [PK_HangFire_Set] PRIMARY KEY CLUSTERED ([Id] ASC) 210 | ); 211 | PRINT 'Created table [HangFire].[Set]'; 212 | 213 | CREATE UNIQUE NONCLUSTERED INDEX [UX_HangFire_Set_KeyAndValue] ON [HangFire].[Set] ( 214 | [Key] ASC, 215 | [Value] ASC 216 | ); 217 | PRINT 'Created index [UX_HangFire_Set_KeyAndValue]'; 218 | 219 | CREATE TABLE [HangFire].[Value]( 220 | [Id] [int] IDENTITY(1,1) NOT NULL, 221 | [Key] [nvarchar](100) NOT NULL, 222 | [StringValue] [nvarchar](max) NULL, 223 | [IntValue] [int] NULL, 224 | [ExpireAt] [datetime] NULL, 225 | 226 | CONSTRAINT [PK_HangFire_Value] PRIMARY KEY CLUSTERED ( 227 | [Id] ASC 228 | ) 229 | ); 230 | PRINT 'Created table [HangFire].[Value]'; 231 | 232 | CREATE UNIQUE NONCLUSTERED INDEX [UX_HangFire_Value_Key] ON [HangFire].[Value] ( 233 | [Key] ASC 234 | ); 235 | PRINT 'Created index [UX_HangFire_Value_Key]'; 236 | 237 | CREATE TABLE [HangFire].[Counter]( 238 | [Id] [int] IDENTITY(1,1) NOT NULL, 239 | [Key] [nvarchar](100) NOT NULL, 240 | [Value] [tinyint] NOT NULL, 241 | [ExpireAt] [datetime] NULL, 242 | 243 | CONSTRAINT [PK_HangFire_Counter] PRIMARY KEY CLUSTERED ([Id] ASC) 244 | ); 245 | PRINT 'Created table [HangFire].[Counter]'; 246 | 247 | CREATE NONCLUSTERED INDEX [IX_HangFire_Counter_Key] ON [HangFire].[Counter] ([Key] ASC) 248 | INCLUDE ([Value]); 249 | PRINT 'Created index [IX_HangFire_Counter_Key]'; 250 | 251 | SET @CURRENT_SCHEMA_VERSION = 1; 252 | END 253 | 254 | IF @CURRENT_SCHEMA_VERSION = 1 255 | BEGIN 256 | PRINT 'Installing schema version 2'; 257 | 258 | -- https://github.com/odinserj/HangFire/issues/83 259 | 260 | DROP INDEX [IX_HangFire_Counter_Key] ON [HangFire].[Counter]; 261 | 262 | ALTER TABLE [HangFire].[Counter] ALTER COLUMN [Value] SMALLINT NOT NULL; 263 | 264 | CREATE NONCLUSTERED INDEX [IX_HangFire_Counter_Key] ON [HangFire].[Counter] ([Key] ASC) 265 | INCLUDE ([Value]); 266 | PRINT 'Index [IX_HangFire_Counter_Key] re-created'; 267 | 268 | DROP TABLE [HangFire].[Value]; 269 | DROP TABLE [HangFire].[Hash]; 270 | PRINT 'Dropped tables [HangFire].[Value] and [HangFire].[Hash]' 271 | 272 | DELETE FROM [HangFire].[Server] WHERE [LastHeartbeat] IS NULL; 273 | ALTER TABLE [HangFire].[Server] ALTER COLUMN [LastHeartbeat] DATETIME NOT NULL; 274 | 275 | SET @CURRENT_SCHEMA_VERSION = 2; 276 | END 277 | 278 | IF @CURRENT_SCHEMA_VERSION = 2 279 | BEGIN 280 | PRINT 'Installing schema version 3'; 281 | 282 | DROP INDEX [IX_HangFire_JobQueue_JobIdAndQueue] ON [HangFire].[JobQueue]; 283 | PRINT 'Dropped index [IX_HangFire_JobQueue_JobIdAndQueue]'; 284 | 285 | CREATE TABLE [HangFire].[Hash]( 286 | [Id] [int] IDENTITY(1,1) NOT NULL, 287 | [Key] [nvarchar](100) NOT NULL, 288 | [Field] [nvarchar](100) NOT NULL, 289 | [Value] [nvarchar](max) NULL, 290 | [ExpireAt] [datetime2](7) NULL, 291 | 292 | CONSTRAINT [PK_HangFire_Hash] PRIMARY KEY CLUSTERED ([Id] ASC) 293 | ); 294 | PRINT 'Created table [HangFire].[Hash]'; 295 | 296 | CREATE UNIQUE NONCLUSTERED INDEX [UX_HangFire_Hash_Key_Field] ON [HangFire].[Hash] ( 297 | [Key] ASC, 298 | [Field] ASC 299 | ); 300 | PRINT 'Created index [UX_HangFire_Hash_Key_Field]'; 301 | 302 | SET @CURRENT_SCHEMA_VERSION = 3; 303 | END 304 | 305 | IF @CURRENT_SCHEMA_VERSION = 3 306 | BEGIN 307 | PRINT 'Installing schema version 4'; 308 | 309 | CREATE TABLE [HangFire].[AggregatedCounter] ( 310 | [Id] [int] IDENTITY(1,1) NOT NULL, 311 | [Key] [nvarchar](100) NOT NULL, 312 | [Value] [bigint] NOT NULL, 313 | [ExpireAt] [datetime] NULL, 314 | 315 | CONSTRAINT [PK_HangFire_CounterAggregated] PRIMARY KEY CLUSTERED ([Id] ASC) 316 | ); 317 | PRINT 'Created table [HangFire].[AggregatedCounter]'; 318 | 319 | CREATE UNIQUE NONCLUSTERED INDEX [UX_HangFire_CounterAggregated_Key] ON [HangFire].[AggregatedCounter] ( 320 | [Key] ASC 321 | ) INCLUDE ([Value]); 322 | PRINT 'Created index [UX_HangFire_CounterAggregated_Key]'; 323 | 324 | CREATE NONCLUSTERED INDEX [IX_HangFire_Hash_ExpireAt] ON [HangFire].[Hash] ([ExpireAt]) 325 | INCLUDE ([Id]); 326 | 327 | CREATE NONCLUSTERED INDEX [IX_HangFire_Job_ExpireAt] ON [HangFire].[Job] ([ExpireAt]) 328 | INCLUDE ([Id]); 329 | 330 | CREATE NONCLUSTERED INDEX [IX_HangFire_List_ExpireAt] ON [HangFire].[List] ([ExpireAt]) 331 | INCLUDE ([Id]); 332 | 333 | CREATE NONCLUSTERED INDEX [IX_HangFire_Set_ExpireAt] ON [HangFire].[Set] ([ExpireAt]) 334 | INCLUDE ([Id]); 335 | 336 | PRINT 'Created indexes for [ExpireAt] columns'; 337 | 338 | CREATE NONCLUSTERED INDEX [IX_HangFire_Hash_Key] ON [HangFire].[Hash] ([Key] ASC) 339 | INCLUDE ([ExpireAt]); 340 | PRINT 'Created index [IX_HangFire_Hash_Key]'; 341 | 342 | CREATE NONCLUSTERED INDEX [IX_HangFire_List_Key] ON [HangFire].[List] ([Key] ASC) 343 | INCLUDE ([ExpireAt], [Value]); 344 | PRINT 'Created index [IX_HangFire_List_Key]'; 345 | 346 | CREATE NONCLUSTERED INDEX [IX_HangFire_Set_Key] ON [HangFire].[Set] ([Key] ASC) 347 | INCLUDE ([ExpireAt], [Value]); 348 | PRINT 'Created index [IX_HangFire_Set_Key]'; 349 | 350 | SET @CURRENT_SCHEMA_VERSION = 4; 351 | END 352 | 353 | IF @CURRENT_SCHEMA_VERSION = 4 354 | BEGIN 355 | PRINT 'Installing schema version 5'; 356 | 357 | DROP INDEX [IX_HangFire_JobQueue_QueueAndFetchedAt] ON [HangFire].[JobQueue]; 358 | PRINT 'Dropped index [IX_HangFire_JobQueue_QueueAndFetchedAt] to modify the [HangFire].[JobQueue].[Queue] column'; 359 | 360 | ALTER TABLE [HangFire].[JobQueue] ALTER COLUMN [Queue] NVARCHAR (50) NOT NULL; 361 | PRINT 'Modified [HangFire].[JobQueue].[Queue] length to 50'; 362 | 363 | CREATE NONCLUSTERED INDEX [IX_HangFire_JobQueue_QueueAndFetchedAt] ON [HangFire].[JobQueue] ( 364 | [Queue] ASC, 365 | [FetchedAt] ASC 366 | ); 367 | PRINT 'Re-created index [IX_HangFire_JobQueue_QueueAndFetchedAt]'; 368 | 369 | ALTER TABLE [HangFire].[Server] DROP CONSTRAINT [PK_HangFire_Server] 370 | PRINT 'Dropped constraint [PK_HangFire_Server] to modify the [HangFire].[Server].[Id] column'; 371 | 372 | ALTER TABLE [HangFire].[Server] ALTER COLUMN [Id] NVARCHAR (100) NOT NULL; 373 | PRINT 'Modified [HangFire].[Server].[Id] length to 100'; 374 | 375 | ALTER TABLE [HangFire].[Server] ADD CONSTRAINT [PK_HangFire_Server] PRIMARY KEY CLUSTERED 376 | ( 377 | [Id] ASC 378 | ); 379 | PRINT 'Re-created constraint [PK_HangFire_Server]'; 380 | 381 | SET @CURRENT_SCHEMA_VERSION = 5; 382 | END 383 | 384 | /*IF @CURRENT_SCHEMA_VERSION = 5 385 | BEGIN 386 | PRINT 'Installing schema version 6'; 387 | 388 | -- Insert migration here 389 | 390 | SET @CURRENT_SCHEMA_VERSION = 6; 391 | END*/ 392 | 393 | UPDATE [HangFire].[Schema] SET [Version] = @CURRENT_SCHEMA_VERSION 394 | IF @@ROWCOUNT = 0 395 | INSERT INTO [HangFire].[Schema] ([Version]) VALUES (@CURRENT_SCHEMA_VERSION) 396 | 397 | PRINT 'Hangfire database schema installed'; 398 | 399 | COMMIT TRANSACTION; 400 | PRINT 'Hangfire SQL objects installed'; 401 | 402 | END TRY 403 | BEGIN CATCH 404 | DECLARE @ERROR NVARCHAR(MAX); 405 | SET @ERROR = ERROR_MESSAGE(); 406 | 407 | if @@TRANCOUNT > 0 408 | ROLLBACK TRANSACTION 409 | 410 | RAISERROR(N'Hangfire database migration script failed: %s Changes were rolled back, please fix the problem and re-run the script again.', 11, 1, @ERROR); 411 | END CATCH 412 | -------------------------------------------------------------------------------- /src/TaskScheduler/Core/ActionRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using Newtonsoft.Json.Linq; 8 | using NLog; 9 | using RestSharp; 10 | using TaskScheduler.Helper; 11 | using TaskScheduler.Jobs; 12 | using TaskScheduler.NotificationServices; 13 | 14 | namespace TaskScheduler.Core 15 | { 16 | public static class ActionRunner 17 | { 18 | #region Properties 19 | 20 | private static readonly Logger Nlogger = LogManager.GetCurrentClassLogger(); 21 | 22 | #endregion 23 | 24 | #region Action Methods 25 | 26 | public static string CallRestApi( 27 | string url, 28 | string httpMethod, 29 | IDictionary headers, 30 | IDictionary queryParameters, 31 | IDictionary parameters, 32 | string body) 33 | { 34 | try 35 | { 36 | if (string.IsNullOrEmpty(url)) return "The url is empty!"; 37 | var client = new RestClient(url); 38 | 39 | if (string.IsNullOrEmpty(httpMethod)) httpMethod = "GET"; 40 | var method = (Method) Enum.Parse(typeof(Method), httpMethod.ToUpper()); 41 | var request = new RestRequest(method); 42 | 43 | var resourceUrl = ""; 44 | 45 | if (headers?.Any() == true) 46 | foreach (var head in headers) 47 | request.AddHeader(head.Key, head.Value); // adds URL headers 48 | 49 | if (queryParameters?.Any() == true) 50 | { 51 | resourceUrl += "?"; 52 | 53 | foreach (var query in queryParameters) 54 | { 55 | request.AddQueryParameter(query.Key, 56 | query.Value); // adds URL querystring like: baseUrl/?name=value&name2=value2 57 | resourceUrl += $"{query.Key}={query.Value}&"; 58 | } 59 | 60 | resourceUrl = resourceUrl.Substring(0, resourceUrl.Length - 1); 61 | } 62 | 63 | if (parameters?.Any() == true) 64 | foreach (var parameter in parameters) 65 | request.AddParameter(parameter.Key, parameter.Value, 66 | ParameterType.RequestBody); // adds URL parameters based on Method 67 | 68 | // add HTTP Headers 69 | if (body != null) 70 | request.AddJsonBody(body); 71 | 72 | Nlogger.Info($"{method}: {url}/{resourceUrl}"); 73 | 74 | // execute the request 75 | var response = client.Execute(request); 76 | var content = $"{response.StatusCode}: {response.Content}"; // raw content as string 77 | 78 | return content; 79 | } 80 | catch (Exception exp) 81 | { 82 | Nlogger.Fatal(exp); 83 | return exp.Message; 84 | } 85 | } 86 | 87 | public static string KillProgress(string process) 88 | { 89 | var result = "Process killing status:\n\r"; 90 | 91 | foreach (var proc in Process.GetProcessesByName(process)) 92 | try 93 | { 94 | proc.Kill(); 95 | result += $"{proc.Id}: {proc.ProcessName} killed at {DateTime.Now:s}"; 96 | } 97 | catch (Exception exp) 98 | { 99 | Nlogger.Fatal(exp); 100 | result += $"{proc.Id}: {proc.ProcessName} {exp.Message}"; 101 | } 102 | finally 103 | { 104 | result += Environment.NewLine; 105 | } 106 | 107 | return result; 108 | } 109 | 110 | public static string SendEmail(string receiver, string message, string subject) 111 | { 112 | try 113 | { 114 | Nlogger.Info($"Sending email to [{receiver}] ..."); 115 | var emailService = NotificationType.Email.Factory(); 116 | var result = emailService.Send(receiver, message, subject); 117 | Nlogger.Info($"Send email successfully completed to {receiver} by result message: {result.Message}"); 118 | return result.Message; 119 | } 120 | catch (Exception exp) 121 | { 122 | Nlogger.Fatal(exp); 123 | return exp.Message; 124 | } 125 | } 126 | 127 | public static string SendSms(string receiver, string message, string subject = null) 128 | { 129 | try 130 | { 131 | Nlogger.Info($"Sending SMS to [{receiver}] ..."); 132 | var smsService = NotificationType.Sms.Factory(); 133 | var result = smsService.Send(receiver, message, subject); 134 | Nlogger.Info($"Send SMS successfully completed to {receiver} by result message: {result.Message}"); 135 | return result.Message; 136 | } 137 | catch (Exception exp) 138 | { 139 | Nlogger.Fatal(exp); 140 | return exp.Message; 141 | } 142 | } 143 | 144 | public static string StartProgram( 145 | string fileName, 146 | string arguments, 147 | string windowsStyle, 148 | bool createNoWindow, 149 | bool useShellExecute) 150 | { 151 | try 152 | { 153 | if (string.IsNullOrEmpty(fileName)) 154 | throw new FileNotFoundException("The file path is empty!", fileName); 155 | 156 | fileName = fileName.Replace(@"\\", @"\"); 157 | arguments = arguments?.Replace(@"\\", @"\"); 158 | 159 | var style = string.IsNullOrEmpty(windowsStyle) 160 | ? ProcessWindowStyle.Normal 161 | : (ProcessWindowStyle) Enum.Parse(typeof(ProcessWindowStyle), windowsStyle, true); 162 | 163 | // Prepare the process to run 164 | var start = new ProcessStartInfo 165 | { 166 | // Enter in the command line arguments, everything you would enter after the executable name itself 167 | Arguments = 168 | Environment.ExpandEnvironmentVariables( 169 | arguments ?? ""), // Replace specifial folders name by real paths 170 | // 171 | // Enter the executable to run, including the complete path 172 | FileName = 173 | Environment.ExpandEnvironmentVariables( 174 | fileName), // Replace specifial folders name by real paths 175 | // 176 | // Do you want to show a console window? 177 | WindowStyle = style, 178 | CreateNoWindow = createNoWindow, 179 | UseShellExecute = useShellExecute 180 | }; 181 | 182 | Nlogger.Info($"Starting [{fileName}] ..."); 183 | var proc = Process.Start(start); 184 | var interval = 500; 185 | while (!proc?.HasExited == true) 186 | { 187 | Nlogger.Info("Sleep for process ending ..."); 188 | System.Threading.Thread.Sleep(interval); 189 | interval += 1000; 190 | if (interval > 5000) 191 | break; 192 | } 193 | 194 | return proc?.HasExited == true ? proc?.ExitCode.ToString() : "1"; 195 | } 196 | catch (Exception exp) 197 | { 198 | Nlogger.Fatal(exp); 199 | return exp.Message; 200 | } 201 | } 202 | 203 | #endregion 204 | 205 | #region Core Methods 206 | 207 | public static string Run(this IJob job) 208 | { 209 | Nlogger.Info($"Runing `{job.ActionName}({job.Name})` action ..."); 210 | 211 | try 212 | { 213 | var methods = typeof(ActionRunner).GetMethods(BindingFlags.Static | BindingFlags.Public); 214 | var targetMethod = 215 | methods.FirstOrDefault(m => 216 | string.Equals(m.Name, job.ActionName, StringComparison.OrdinalIgnoreCase)); 217 | 218 | if (targetMethod == null) throw new MissingMethodException(nameof(ActionRunner), job.ActionName); 219 | 220 | // sort args according by method parameters orders 221 | var targetMethodParameters = targetMethod.GetParameters(); 222 | var orderedArgs = new object[targetMethodParameters.Length]; 223 | for (var i = 0; i < targetMethodParameters.Length; i++) 224 | if (job.ActionParameters.ContainsKey(targetMethodParameters[i].Name)) 225 | { 226 | // The parameter provided by given args 227 | var obj = job.ActionParameters[targetMethodParameters[i].Name]; 228 | if (obj?.GetType() == typeof(JObject)) 229 | orderedArgs[i] = ((JObject) obj).ToObject(targetMethodParameters[i].ParameterType); 230 | else orderedArgs[i] = obj; 231 | } 232 | else if (targetMethodParameters[i].HasDefaultValue) // for optional parameter 233 | { 234 | orderedArgs[i] = targetMethodParameters[i].DefaultValue; 235 | } 236 | else // create default value 237 | { 238 | orderedArgs[i] = targetMethodParameters[i].ParameterType.GetDefault(); 239 | } 240 | 241 | var result = targetMethod.Invoke(null, orderedArgs); 242 | 243 | Nlogger.Info($"Result of {job.ActionName}({job.Name}): {result}"); 244 | Nlogger.Info($"The `{job.ActionName}({job.Name})` action completed\n"); 245 | 246 | return result?.ToString(); 247 | } 248 | catch (Exception exp) 249 | { 250 | Nlogger.Fatal(exp); 251 | return exp.Message; 252 | } 253 | } 254 | 255 | internal static object GetDefault(this Type type) 256 | { 257 | return type.IsValueType ? Activator.CreateInstance(type) : null; 258 | } 259 | 260 | #endregion 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/TaskScheduler/Core/JobsManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Hangfire; 6 | using Hangfire.Storage; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | using NLog; 10 | using TaskScheduler.Helper; 11 | using TaskScheduler.Jobs; 12 | using TaskScheduler.NotificationServices; 13 | 14 | namespace TaskScheduler.Core 15 | { 16 | public static class JobsManager 17 | { 18 | #region Properties 19 | 20 | private static string JsonSetting { get; set; } = ""; 21 | private static readonly Logger Nlogger = LogManager.GetCurrentClassLogger(); 22 | public static SettingWrapper Setting { get; set; } 23 | 24 | #endregion 25 | 26 | 27 | #region Constructors 28 | 29 | /// 30 | /// Check setting file and reset jobs if changed. 31 | /// 32 | /// setting file path. 33 | /// next checkup time by minute interval 34 | public static void CheckupSetting(string settingPath, int nextCheckupInterval) 35 | { 36 | var settingChanged = false; 37 | try 38 | { 39 | var setting = FileManager.ReadFileSafely(settingPath); 40 | 41 | if (string.IsNullOrEmpty(setting)) 42 | { 43 | Nlogger.Warn("Setting file not found!"); 44 | CreateSettingFile(settingPath); 45 | CheckupSetting(settingPath, nextCheckupInterval); 46 | return; 47 | } 48 | 49 | if (setting.Equals(JsonSetting, StringComparison.OrdinalIgnoreCase)) 50 | { 51 | Nlogger.Info("Setting was not changed."); 52 | return; 53 | } 54 | Nlogger.Info("Setting was changed, reset jobs..."); 55 | settingChanged = true; 56 | 57 | // clear old jobs 58 | ClearAllRecurringJobs(); 59 | PurgeJobs(); 60 | // 61 | // Add new jobs from setting file 62 | JsonSetting = setting; 63 | Setting = DeserializeFromJson(JsonSetting); 64 | 65 | RegisterJobs(); 66 | 67 | Nlogger.Info("Jobs are ready to work."); 68 | } 69 | catch (Exception exp) 70 | { 71 | Nlogger.Fatal(exp); 72 | } 73 | finally 74 | { 75 | if (settingChanged) 76 | RecurringJob.AddOrUpdate(() => CheckupSetting(settingPath, nextCheckupInterval), 77 | Cron.MinuteInterval(nextCheckupInterval), TimeZoneInfo.Local); 78 | } 79 | } 80 | 81 | #endregion 82 | 83 | 84 | 85 | #region Methods 86 | 87 | public static void RegisterJobs() 88 | { 89 | if (Setting?.Jobs?.Any() != true) return; 90 | 91 | foreach (var job in Setting.Jobs) job?.Register(); 92 | } 93 | 94 | public static IJob AddJob(IJob job) 95 | { 96 | Setting.Jobs.Add(job); 97 | job.Register(); 98 | return job; 99 | } 100 | 101 | public static string SerializeToJson() 102 | { 103 | return Setting == null ? "" : JsonConvert.SerializeObject(Setting, Formatting.Indented); 104 | } 105 | 106 | public static SettingWrapper DeserializeFromJson(string json) 107 | { 108 | var result = new SettingWrapper(); 109 | 110 | try 111 | { 112 | var asm = Assembly.GetAssembly(typeof(IJob)); 113 | json = json.Replace("{#version}", asm.GetName().Version.ToString(3)); 114 | var setting = JsonConvert.DeserializeObject(json); 115 | 116 | result.Jobs = new List(); 117 | result.NotificationServices = new List(); 118 | result.Notifications = setting.Notifications; 119 | 120 | if (setting.Jobs == null || setting.NotificationServices == null) return null; 121 | // 122 | // parse notification services to real type 123 | foreach (JObject ns in setting.NotificationServices) 124 | { 125 | try 126 | { 127 | var serviceType = ns.GetValue("notificationType")?.ToString(); 128 | if (string.IsNullOrWhiteSpace(serviceType)) continue; 129 | 130 | var service = asm.GetTypes() 131 | .FirstOrDefault(t => t.Name.Equals(serviceType, StringComparison.OrdinalIgnoreCase)); 132 | var serviceObj = (INotificationService)ns.ToObject(service); 133 | serviceObj?.Initial(); 134 | result.NotificationServices.Add(serviceObj); 135 | } 136 | catch (Exception e) 137 | { 138 | Nlogger.Error(e); 139 | } 140 | } 141 | // 142 | // parse jobs to real type 143 | foreach (JObject j in setting.Jobs) 144 | { 145 | try 146 | { 147 | var jobTypeName = j.GetValue("jobType")?.ToString(); 148 | 149 | if (string.IsNullOrWhiteSpace(jobTypeName)) continue; 150 | 151 | var jobType = asm.GetTypes() 152 | .FirstOrDefault(t => t.Name.Equals(jobTypeName, StringComparison.OrdinalIgnoreCase)); 153 | var jobObj = (IJob)j.ToObject(jobType); 154 | // 155 | // if the job hasn't any notification and exist at last one public notification 156 | // then insert public notifications 157 | if (jobObj != null && jobObj.Notifications?.Any() != true && 158 | setting.Notifications?.Any() == true) jobObj.Notifications = setting.Notifications; 159 | result.Jobs.Add(jobObj); 160 | } 161 | catch (Exception e) 162 | { 163 | Nlogger.Error(e); 164 | } 165 | } 166 | } 167 | catch (Exception exp) 168 | { 169 | Nlogger.Fatal(exp); 170 | } 171 | 172 | return result; 173 | } 174 | 175 | public static void ClearAllRecurringJobs() 176 | { 177 | using (var connection = JobStorage.Current.GetConnection()) 178 | { 179 | foreach (var recurringJob in connection.GetRecurringJobs()) 180 | RecurringJob.RemoveIfExists(recurringJob.Id); 181 | } 182 | } 183 | 184 | public static void PurgeJobs() 185 | { 186 | var monitor = JobStorage.Current?.GetMonitoringApi(); 187 | var toDelete = new List(); 188 | 189 | if (monitor == null) return; 190 | 191 | foreach (var queue in monitor.Queues()) 192 | for (var i = 0; i < Math.Ceiling(queue.Length / 1000d); i++) 193 | monitor.EnqueuedJobs(queue.Name, 1000 * i, 1000) 194 | .ForEach(x => toDelete.Add(x.Key)); 195 | 196 | foreach (var jobId in toDelete) BackgroundJob.Delete(jobId); 197 | } 198 | 199 | public static void CreateSettingFile(string settingPath) 200 | { 201 | Nlogger.Info("Trying to create setting file..."); 202 | FileManager.WriteFileSafely(settingPath, JsonConvert.SerializeObject(new JobsSetting())); 203 | Nlogger.Info($"Setting file successfully created at:\t{settingPath}"); 204 | } 205 | 206 | #endregion 207 | } 208 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Core/LogEverythingAttribute.cs: -------------------------------------------------------------------------------- 1 | using Hangfire.Client; 2 | using Hangfire.Common; 3 | using Hangfire.Logging; 4 | using Hangfire.Server; 5 | using Hangfire.States; 6 | using Hangfire.Storage; 7 | 8 | namespace Hasin.Taaghche.TaskScheduler.Core 9 | { 10 | public class LogEverythingAttribute : JobFilterAttribute, IApplyStateFilter 11 | { 12 | private static readonly ILog Logger = LogProvider.GetCurrentClassLogger(); 13 | 14 | public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction) 15 | { 16 | Logger.InfoFormat( 17 | $"Job `{context.BackgroundJob?.Id}` state was changed from `{context.OldStateName}` to `{context.NewState?.Name}`"); 18 | } 19 | 20 | public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction) 21 | { 22 | Logger.InfoFormat($"Job `{context.BackgroundJob?.Id}` state `{context.OldStateName}` was unapplied."); 23 | } 24 | 25 | public void OnCreating(CreatingContext context) 26 | { 27 | Logger.InfoFormat($"Creating a job based on method `{context.Job?.Method?.Name}`..."); 28 | } 29 | 30 | public void OnCreated(CreatedContext context) 31 | { 32 | Logger.InfoFormat( 33 | $"Job that is based on method `{context.Job?.Method?.Name}` has been created with id `{context.BackgroundJob?.Id}`"); 34 | } 35 | 36 | public void OnPerforming(PerformingContext context) 37 | { 38 | Logger.InfoFormat($"Starting to perform job `{context.BackgroundJob?.Id}`"); 39 | } 40 | 41 | public void OnPerformed(PerformedContext context) 42 | { 43 | Logger.InfoFormat($"Job `{context.BackgroundJob?.Id}` has been performed"); 44 | } 45 | 46 | public void OnStateElection(ElectStateContext context) 47 | { 48 | var failedState = context.CandidateState as FailedState; 49 | if (failedState != null) 50 | Logger.WarnFormat( 51 | $"Job `{context.BackgroundJob?.Id}` has been failed due to an exception `{failedState.Exception?.Message}`"); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Core/LogFailureAttribute.cs: -------------------------------------------------------------------------------- 1 | using Hangfire.Client; 2 | using Hangfire.Common; 3 | using Hangfire.Logging; 4 | using Hangfire.Server; 5 | using Hangfire.States; 6 | using Hangfire.Storage; 7 | 8 | namespace TaskScheduler.Core 9 | { 10 | public class LogEverythingAttribute : JobFilterAttribute, IApplyStateFilter 11 | { 12 | private static readonly ILog Logger = LogProvider.GetCurrentClassLogger(); 13 | 14 | public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction) 15 | { 16 | Logger.InfoFormat( 17 | $"Job `{context.BackgroundJob?.Id}` state was changed from `{context.OldStateName}` to `{context.NewState?.Name}`"); 18 | } 19 | 20 | public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction) 21 | { 22 | Logger.InfoFormat($"Job `{context.BackgroundJob?.Id}` state `{context.OldStateName}` was unapplied."); 23 | } 24 | 25 | public void OnCreating(CreatingContext context) 26 | { 27 | Logger.InfoFormat($"Creating a job based on method `{context.Job?.Method?.Name}`..."); 28 | } 29 | 30 | public void OnCreated(CreatedContext context) 31 | { 32 | Logger.InfoFormat( 33 | $"Job that is based on method `{context.Job?.Method?.Name}` has been created with id `{context.BackgroundJob?.Id}`"); 34 | } 35 | 36 | public void OnPerforming(PerformingContext context) 37 | { 38 | Logger.InfoFormat($"Starting to perform job `{context.BackgroundJob?.Id}`"); 39 | } 40 | 41 | public void OnPerformed(PerformedContext context) 42 | { 43 | Logger.InfoFormat($"Job `{context.BackgroundJob?.Id}` has been performed"); 44 | } 45 | 46 | public void OnStateElection(ElectStateContext context) 47 | { 48 | if (context.CandidateState is FailedState failedState) 49 | Logger.WarnFormat( 50 | $"Job `{context.BackgroundJob?.Id}` has been failed due to an exception `{failedState.Exception?.Message}`"); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Helper/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace TaskScheduler.Helper 5 | { 6 | /// 7 | /// Gets the values from the AssemblyInfo.cs file for the current executing assembly 8 | /// 9 | /// 10 | /// string company = AssemblyInfo.Company; 11 | /// string product = AssemblyInfo.Product; 12 | /// string copyright = AssemblyInfo.Copyright; 13 | /// string trademark = AssemblyInfo.Trademark; 14 | /// string title = AssemblyInfo.Title; 15 | /// string description = AssemblyInfo.Description; 16 | /// string configuration = AssemblyInfo.Configuration; 17 | /// string fileVersion = AssemblyInfo.FileVersion; 18 | /// Version = AssemblyInfo.Version; 19 | /// string versionFull = AssemblyInfo.VersionFull; 20 | /// string versionMajor = AssemblyInfo.VersionMajor; 21 | /// string versionMinor = AssemblyInfo.VersionMinor; 22 | /// string versionBuild = AssemblyInfo.VersionBuild; 23 | /// string versionRevision = AssemblyInfo.VersionRevision; 24 | /// 25 | public static class AssemblyInfo 26 | { 27 | public static string Company 28 | { 29 | get { return GetExecutingAssemblyAttribute(a => a.Company); } 30 | } 31 | 32 | public static string Product 33 | { 34 | get { return GetExecutingAssemblyAttribute(a => a.Product); } 35 | } 36 | 37 | public static string Copyright 38 | { 39 | get { return GetExecutingAssemblyAttribute(a => a.Copyright); } 40 | } 41 | 42 | public static string Trademark 43 | { 44 | get { return GetExecutingAssemblyAttribute(a => a.Trademark); } 45 | } 46 | 47 | public static string Title 48 | { 49 | get { return GetExecutingAssemblyAttribute(a => a.Title); } 50 | } 51 | 52 | public static string Description 53 | { 54 | get { return GetExecutingAssemblyAttribute(a => a.Description); } 55 | } 56 | 57 | public static string Configuration 58 | { 59 | get { return GetExecutingAssemblyAttribute(a => a.Description); } 60 | } 61 | 62 | public static string FileVersion 63 | { 64 | get { return GetExecutingAssemblyAttribute(a => a.Version); } 65 | } 66 | 67 | public static Version Version => Assembly.GetExecutingAssembly().GetName().Version; 68 | public static string VersionFull => Version.ToString(); 69 | public static string VersionMajor => Version.Major.ToString(); 70 | public static string VersionMinor => Version.Minor.ToString(); 71 | public static string VersionBuild => Version.Build.ToString(); 72 | public static string VersionRevision => Version.Revision.ToString(); 73 | 74 | private static string GetExecutingAssemblyAttribute(Func value) where T : Attribute 75 | { 76 | var attribute = (T)Attribute.GetCustomAttribute(Assembly.GetExecutingAssembly(), typeof(T)); 77 | return value.Invoke(attribute); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/TaskScheduler/Helper/DatabaseHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using AdoManager; 4 | using Dapper; 5 | using NLog; 6 | 7 | namespace TaskScheduler.Helper 8 | { 9 | public static class DatabaseHelper 10 | { 11 | private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); 12 | 13 | 14 | public static bool IsExistDatabase(this ConnectionManager cm) 15 | { 16 | try 17 | { 18 | if (string.IsNullOrEmpty(cm.Connection.DatabaseName)) 19 | { 20 | var dbName = GetPathFromAttachDbFilename(cm.Connection.AttachDbFilename); 21 | 22 | Logger.Info($"Checking the database file exist in this path: [{dbName}] ..."); 23 | // try to connect to database if can to connect that then is shown database is exist! 24 | if (!File.Exists(dbName)) 25 | { 26 | Logger.Info($"The [{Path.GetFileNameWithoutExtension(dbName)}] database was not found!"); 27 | return false; 28 | } 29 | 30 | cm.Open(); 31 | cm.Close(); 32 | Logger.Info( 33 | $"Ok, Connected to [{Path.GetFileNameWithoutExtension(dbName)}] database successfully."); 34 | return true; 35 | } 36 | 37 | Logger.Info($"Checking the [{cm.Connection.DatabaseName}] database is exist ..."); 38 | // try to connect to database if can to connect that then is shown database is exist! 39 | cm.Open(); 40 | cm.Close(); 41 | Logger.Info($"Ok, Connected to [{cm.Connection.DatabaseName}] database successfully."); 42 | return true; 43 | } 44 | catch (Exception exp) 45 | { 46 | Logger.Error(exp); 47 | Logger.Info("The database was not found!"); 48 | return false; 49 | } 50 | } 51 | 52 | public static bool CreateDatabase(this ConnectionManager cm) 53 | { 54 | try 55 | { 56 | Logger.Info("Trying to recreate the database ..."); 57 | 58 | var server = cm.Connection.Server; 59 | 60 | using (var masterConn = new ConnectionManager(new Connection(server, "master"))) 61 | { 62 | if (string.IsNullOrEmpty(cm.Connection.AttachDbFilename)) // Sql Server 63 | { 64 | masterConn.SqlConn.Execute($"CREATE DATABASE {cm.Connection.DatabaseName}"); 65 | Logger.Info($"The database [{cm.Connection.DatabaseName}] created successfully."); 66 | return IsExistDatabase(cm); 67 | } 68 | 69 | var dbFullPath = GetPathFromAttachDbFilename(cm.Connection.AttachDbFilename); 70 | 71 | FileManager.CheckupDirectory(dbFullPath); 72 | 73 | var dbName = Path.GetFileNameWithoutExtension(dbFullPath); 74 | var dbLdfFullPath = Path.ChangeExtension(dbFullPath, ".ldf"); 75 | 76 | var sql = $@"CREATE DATABASE [{dbName}] 77 | ON PRIMARY (NAME={dbName?.Replace(" ", "_")}_data, FILENAME = '{dbFullPath}') 78 | LOG ON (NAME={dbName?.Replace(" ", "_")}_log, FILENAME = '{dbLdfFullPath}')"; 79 | 80 | masterConn.SqlConn.Execute(sql); 81 | 82 | if (!File.Exists(dbFullPath)) return false; 83 | 84 | // Detach database from Sql Server to used in this application only. 85 | masterConn.DetachDatabase(dbName); 86 | 87 | Logger.Info($"The [{dbName}] database created successfully."); 88 | return true; 89 | } 90 | } 91 | catch (Exception exp) 92 | { 93 | Logger.Error(exp); 94 | return false; 95 | } 96 | } 97 | 98 | public static bool CreateDatabaseIfNotExist(this ConnectionManager cm, string continuesScript = null) 99 | { 100 | var result = cm.IsExistDatabase() || cm.CreateDatabase(); 101 | 102 | try 103 | { 104 | if (!string.IsNullOrEmpty(continuesScript) && result) 105 | { 106 | cm.SqlConn.Execute(continuesScript); 107 | Logger.Info("The hangfire scripts executed on database successful."); 108 | } 109 | } 110 | catch (Exception exp) 111 | { 112 | Logger.Info("An exception occurred when execute the hangfire scripts."); 113 | Logger.Error(exp); 114 | result = false; 115 | } 116 | 117 | return result; 118 | } 119 | 120 | private static string GetPathFromAttachDbFilename(string attachDbFilename) 121 | { 122 | return attachDbFilename.Replace("|DataDirectory|", 123 | AppDomain.CurrentDomain.GetData("DataDirectory").ToString()); 124 | } 125 | 126 | public static bool DetachDatabase(this ConnectionManager cm, string dbName) 127 | { 128 | try 129 | { 130 | cm.Connection.DatabaseName = "master"; 131 | cm.SqlConn.Execute($"exec sp_detach_db '{dbName}'"); 132 | return true; 133 | } 134 | catch (Exception exp) 135 | { 136 | Logger.Error(exp); 137 | return false; 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Helper/FileManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Text; 5 | using NLog; 6 | 7 | namespace TaskScheduler.Helper 8 | { 9 | public static class FileManager 10 | { 11 | private static readonly Logger Nlogger = LogManager.GetCurrentClassLogger(); 12 | 13 | public static bool WriteFileSafely(string path, string data) 14 | { 15 | try 16 | { 17 | CheckupDirectory(path); 18 | 19 | //Open the File 20 | File.WriteAllText(path, data, Encoding.UTF8); 21 | 22 | return true; 23 | } 24 | catch (Exception exp) 25 | { 26 | Nlogger.Fatal(exp); 27 | } 28 | 29 | return false; 30 | } 31 | 32 | public static string ReadFileSafely(string path) 33 | { 34 | try 35 | { 36 | return !File.Exists(path) ? null : File.ReadAllText(path, Encoding.UTF8); 37 | } 38 | catch (Exception exp) 39 | { 40 | Nlogger.Fatal(exp); 41 | } 42 | 43 | return null; 44 | } 45 | 46 | public static string ReadResourceFile(string path) 47 | { 48 | var result = string.Empty; 49 | try 50 | { 51 | var assembly = Assembly.GetExecutingAssembly(); 52 | var resourceName = $"{assembly.GetName().Name}.{path}"; 53 | 54 | using (var stream = assembly.GetManifestResourceStream(resourceName)) 55 | { 56 | if (stream != null) 57 | using (var reader = new StreamReader(stream)) 58 | { 59 | result = reader.ReadToEnd(); 60 | return result; 61 | } 62 | } 63 | } 64 | catch (Exception exp) 65 | { 66 | Nlogger.Fatal(exp); 67 | } 68 | 69 | return result; 70 | } 71 | 72 | public static void CheckupDirectory(string path) 73 | { 74 | var pathDir = Path.GetDirectoryName(path); 75 | 76 | if (string.IsNullOrEmpty(pathDir)) return; 77 | 78 | 79 | if (!Directory.Exists(pathDir)) 80 | Directory.CreateDirectory(pathDir); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Helper/HttpHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Http; 2 | using Newtonsoft.Json; 3 | using RestSharp; 4 | 5 | namespace TaskScheduler.Helper 6 | { 7 | internal static class HttpHelper 8 | { 9 | public static bool IsSuccessful(this IRestResponse response) 10 | { 11 | // if status code was 2xx 12 | return (int) response.StatusCode / 100 == 2; 13 | } 14 | 15 | public static T ReadData(this IRestResponse response) 16 | { 17 | if (!response.IsSuccessful()) 18 | throw new HttpResponseException(response.StatusCode); 19 | 20 | return JsonConvert.DeserializeObject(response.Content); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Helper/NotificationHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using TaskScheduler.Core; 4 | using TaskScheduler.NotificationServices; 5 | 6 | namespace TaskScheduler.Helper 7 | { 8 | public static class NotificationHelper 9 | { 10 | public static INotificationService Factory(this NotificationType notificationType) 11 | { 12 | var service = JobsManager.Setting.NotificationServices.FirstOrDefault(ns => 13 | ns.IsDefaultService && ns.NotificationType == notificationType); 14 | 15 | if (service == null) 16 | throw new ArgumentOutOfRangeException(nameof(notificationType), notificationType, null); 17 | 18 | return service; 19 | 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Helper/StringHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace TaskScheduler.Helper 6 | { 7 | public static class StringHelper 8 | { 9 | private static readonly Regex EnglishPattern = new Regex("[a-zA-Z]+", RegexOptions.Compiled); 10 | 11 | private static readonly Regex LowerCaseConvention = 12 | new Regex(@"(?<=[A-Z])(?=[A-Z][a-z]) | (?<=[^A-Z])(?=[A-Z]) | (?<=[A-Za-z])(?=[^A-Za-z])", 13 | RegexOptions.IgnorePatternWhitespace); 14 | 15 | public static string ToLowerCaseNamingConvention(this string s, bool toLowerCase = false) 16 | { 17 | return toLowerCase 18 | ? LowerCaseConvention.Replace(s, " ").ToLower() 19 | : LowerCaseConvention.Replace(s, " "); 20 | } 21 | 22 | public static bool HasEnglishChar(this string s) 23 | { 24 | return !string.IsNullOrEmpty(s) && EnglishPattern.IsMatch(s); 25 | } 26 | 27 | public static string EncodeToBase64(this string plainText) 28 | { 29 | var plainTextBytes = Encoding.UTF8.GetBytes(plainText); 30 | return Convert.ToBase64String(plainTextBytes); 31 | } 32 | 33 | public static string DecodeFromBase64(this string base64EncodedData) 34 | { 35 | var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); 36 | return Encoding.UTF8.GetString(base64EncodedBytes); 37 | } 38 | 39 | public static string[] SplitUp(this string text) 40 | { 41 | return text.Split(new[] {",", ";", " "}, StringSplitOptions.RemoveEmptyEntries); 42 | } 43 | 44 | public static string CleanText(this string text) 45 | { 46 | return text.Replace("`", "").Replace(">", "").Replace("_", "").Replace("*", " "); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Jobs/Delayed.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Hangfire; 4 | using NLog; 5 | 6 | namespace TaskScheduler.Jobs 7 | { 8 | /// 9 | /// Delayed jobs are executed only once too, but not immediately, after a certain time interval. 10 | /// 11 | public class Delayed : Job 12 | { 13 | public override string Register() 14 | { 15 | try 16 | { 17 | JobId = BackgroundJob.Schedule(() => Trigger(this), TriggerOn); 18 | 19 | Nlogger.Info($"The [{JobId}] added to hangfire delayed jobs successful."); 20 | 21 | return JobId; 22 | } 23 | catch (Exception exp) 24 | { 25 | Nlogger.Fatal(exp); 26 | return exp.Message; 27 | } 28 | } 29 | 30 | /// 31 | /// Continuations are executed when its parent job has been finished. 32 | /// 33 | /// The job, which should be continued after this job 34 | public void ContinueWith(Action action) 35 | { 36 | BackgroundJob.ContinueWith(JobId, () => action()); 37 | } 38 | 39 | #region Properties 40 | 41 | private static readonly Logger Nlogger = LogManager.GetCurrentClassLogger(); 42 | 43 | public new JobType JobType 44 | { 45 | get => base.JobType; 46 | protected set => base.JobType = value; 47 | } 48 | 49 | public new TimeSpan TriggerOn 50 | { 51 | get => TimeSpan.ParseExact(base.TriggerOn, "G", CultureInfo.InvariantCulture); 52 | set => base.TriggerOn = value.ToString("G"); 53 | } 54 | 55 | #endregion 56 | 57 | #region Constructors 58 | 59 | public Delayed() 60 | { 61 | JobType = JobType.Delayed; 62 | } 63 | 64 | public Delayed(IJob job) : base(job) 65 | { 66 | JobType = JobType.Delayed; 67 | } 68 | 69 | #endregion 70 | } 71 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Jobs/FireAndForget.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire; 3 | using NLog; 4 | 5 | namespace TaskScheduler.Jobs 6 | { 7 | /// 8 | /// Fire-and-forget jobs are executed only once and almost immediately after creation. 9 | /// 10 | public class FireAndForget : Job 11 | { 12 | public override string Register() 13 | { 14 | try 15 | { 16 | JobId = BackgroundJob.Enqueue(() => Trigger(this)); 17 | 18 | Nlogger.Info($"The [{JobId}] added to hangfire fire and forget jobs successful."); 19 | 20 | return JobId; 21 | } 22 | catch (Exception exp) 23 | { 24 | Nlogger.Fatal(exp); 25 | return exp.Message; 26 | } 27 | } 28 | 29 | /// 30 | /// Continuations are executed when its parent job has been finished. 31 | /// 32 | /// The job, which should be continued after this job 33 | public void ContinueWith(Action action) 34 | { 35 | BackgroundJob.ContinueWith(JobId, () => action()); 36 | } 37 | 38 | #region Properties 39 | 40 | private static readonly Logger Nlogger = LogManager.GetCurrentClassLogger(); 41 | 42 | public new JobType JobType 43 | { 44 | get => JobType.FireAndForget; 45 | protected set => base.JobType = value; 46 | } 47 | 48 | public new string TriggerOn 49 | { 50 | get => null; 51 | protected set => base.TriggerOn = value; 52 | } 53 | 54 | #endregion 55 | 56 | #region Constructors 57 | 58 | public FireAndForget() 59 | { 60 | JobType = JobType.FireAndForget; 61 | } 62 | 63 | public FireAndForget(IJob job) : base(job) 64 | { 65 | JobType = JobType.FireAndForget; 66 | } 67 | 68 | #endregion 69 | } 70 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Jobs/IJob.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | using TaskScheduler.NotificationServices; 5 | 6 | namespace TaskScheduler.Jobs 7 | { 8 | public interface IJob 9 | { 10 | [JsonIgnore] string JobId { get; set; } 11 | 12 | [JsonProperty(PropertyName = "name", NullValueHandling = NullValueHandling.Ignore)] 13 | string Name { get; set; } 14 | 15 | [JsonProperty(PropertyName = "description", NullValueHandling = NullValueHandling.Ignore)] 16 | string Description { get; set; } 17 | 18 | [JsonProperty(PropertyName = "enable", NullValueHandling = NullValueHandling.Ignore)] 19 | bool Enable { get; set; } 20 | 21 | [JsonRequired] 22 | [JsonConverter(typeof(StringEnumConverter))] 23 | [JsonProperty(PropertyName = "jobType", NullValueHandling = NullValueHandling.Include, 24 | Required = Required.Always)] 25 | JobType JobType { get; set; } 26 | 27 | [JsonRequired] 28 | [JsonProperty(PropertyName = "actionName", NullValueHandling = NullValueHandling.Include, 29 | Required = Required.Always)] 30 | string ActionName { get; set; } 31 | 32 | [JsonProperty(PropertyName = "actionParameters", NullValueHandling = NullValueHandling.Include)] 33 | IDictionary ActionParameters { get; set; } 34 | 35 | [JsonProperty(PropertyName = "triggerOn", NullValueHandling = NullValueHandling.Include, 36 | Required = Required.AllowNull)] 37 | string TriggerOn { get; set; } // Cron expression or TimeSpan 38 | 39 | [JsonProperty(PropertyName = "notifications", NullValueHandling = NullValueHandling.Ignore)] 40 | IList Notifications { get; set; } // Notification on result events 41 | 42 | [JsonConverter(typeof(StringEnumConverter))] 43 | [JsonProperty(PropertyName = "notifyCondition", NullValueHandling = NullValueHandling.Include)] 44 | NotifyCondition NotifyCondition { get; set; } 45 | 46 | [JsonProperty(PropertyName = "notifyConditionResult", NullValueHandling = NullValueHandling.Include)] 47 | string NotifyConditionResult { get; set; } 48 | 49 | /// 50 | /// Add or update a job in hangfire to do lists. 51 | /// 52 | /// Get added job id 53 | string Register(); 54 | 55 | /// 56 | /// Run job action now. 57 | /// 58 | void Trigger(Job job); 59 | } 60 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Jobs/Job.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.ServiceModel; 6 | using NLog; 7 | using TaskScheduler.Core; 8 | using TaskScheduler.NotificationServices; 9 | 10 | namespace TaskScheduler.Jobs 11 | { 12 | public class Job : IJob 13 | { 14 | private static Logger Nlogger { get; } = LogManager.GetCurrentClassLogger(); 15 | 16 | #region IJob Implementation 17 | 18 | public string JobId { get; set; } 19 | public bool Enable { get; set; } = true; 20 | public string Name { get; set; } 21 | public string Description { get; set; } 22 | public JobType JobType { get; set; } 23 | public string ActionName { get; set; } 24 | public IDictionary ActionParameters { get; set; } 25 | public string TriggerOn { get; set; } 26 | public IList Notifications { get; set; } 27 | public NotifyCondition NotifyCondition { get; set; } 28 | public string NotifyConditionResult { get; set; } 29 | 30 | 31 | public virtual string Register() 32 | { 33 | throw new ActionNotSupportedException( 34 | "This class is abstract for register job! Use of child classes instead this."); 35 | } 36 | 37 | 38 | public bool CompareByNotifyCondition(string actionResult) 39 | { 40 | switch (NotifyCondition) 41 | { 42 | case NotifyCondition.Equals: 43 | return actionResult.Equals(NotifyConditionResult, StringComparison.OrdinalIgnoreCase); 44 | case NotifyCondition.NotEquals: 45 | return !actionResult.Equals(NotifyConditionResult, StringComparison.OrdinalIgnoreCase); 46 | case NotifyCondition.MoreThan: 47 | return String.Compare(actionResult, NotifyConditionResult, StringComparison.OrdinalIgnoreCase) > 0; 48 | case NotifyCondition.EqualsOrMoreThan: 49 | return String.Compare(actionResult, NotifyConditionResult, StringComparison.OrdinalIgnoreCase) >= 0; 50 | case NotifyCondition.LessThan: 51 | return String.Compare(actionResult, NotifyConditionResult, StringComparison.OrdinalIgnoreCase) < 0; 52 | case NotifyCondition.EqualsOrLessThan: 53 | return String.Compare(actionResult, NotifyConditionResult, StringComparison.OrdinalIgnoreCase) <= 0; 54 | default: 55 | return false; 56 | } 57 | } 58 | 59 | public void Trigger(Job job) 60 | { 61 | MapToThis(job); 62 | if (!Enable) return; 63 | 64 | var result = job.Run(); 65 | OnTriggerNotification(result); 66 | } 67 | 68 | public void OnTriggerNotification(string result) 69 | { 70 | if (Notifications == null || !Notifications.Any() || NotifyCondition == NotifyCondition.None) return; 71 | 72 | var subject = $"{(Debugger.IsAttached ? "`DEBUG MODE`" : "")} **{Name}**\n" + 73 | $"----------------------------------------"; 74 | var body = $"\nResult: `{result}` " + 75 | $"\nAction: `{ActionName}` \n"; 76 | 77 | if (ActionParameters?.Any() == true) 78 | { 79 | body += "Arguments: ```\n"; 80 | foreach (var arg in ActionParameters.Where((k, v) => !String.IsNullOrEmpty(v.ToString()))) 81 | { 82 | var val = string.Concat(arg.Value.ToString() 83 | .Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries) 84 | .Select(x => $"{x}\n\t")); 85 | val = val.Remove(val.LastIndexOf('\t')); 86 | body += $" • {arg.Key}: {val}"; 87 | } 88 | 89 | body = body.Replace("\"", "") + "\n```"; 90 | } 91 | 92 | foreach (var notify in Notifications.Where(n => CompareByNotifyCondition(result))) 93 | try 94 | { 95 | #if !TestWithoutNotify 96 | notify.Notify(body, subject); 97 | #endif 98 | } 99 | catch (Exception exp) 100 | { 101 | Nlogger.Error(exp); 102 | } 103 | } 104 | 105 | public void MapToThis(IJob job) 106 | { 107 | if (job == null) return; 108 | 109 | Name = job.Name; 110 | Description = job.Description; 111 | Enable = job.Enable; 112 | JobType = job.JobType; 113 | ActionName = job.ActionName; 114 | ActionParameters = job.ActionParameters; 115 | Notifications = job.Notifications; 116 | JobId = job.JobId; 117 | TriggerOn = job.TriggerOn; 118 | NotifyCondition = job.NotifyCondition; 119 | NotifyConditionResult = job.NotifyConditionResult; 120 | } 121 | 122 | #endregion 123 | 124 | #region Constructors 125 | 126 | public Job() 127 | { 128 | } 129 | 130 | public Job(IJob job) 131 | { 132 | MapToThis(job); 133 | } 134 | 135 | #endregion 136 | } 137 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Jobs/JobType.cs: -------------------------------------------------------------------------------- 1 | namespace TaskScheduler.Jobs 2 | { 3 | /// 4 | /// Hangfire Job Types 5 | /// 6 | public enum JobType 7 | { 8 | /// 9 | /// Fire-and-forget jobs are executed only once and almost immediately after creation. 10 | /// Method: 11 | /// 12 | FireAndForget = 1, 13 | 14 | /// 15 | /// Delayed jobs are executed only once too, but not immediately, after a certain time interval. 16 | /// Method: 17 | /// 18 | Delayed = 2, 19 | 20 | /// 21 | /// Recurring jobs fire many times on the specified schedule. 22 | /// Method: 23 | /// 24 | Recurring = 3 25 | } 26 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Jobs/JobsSetting.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | using TaskScheduler.NotificationServices; 4 | 5 | namespace TaskScheduler.Jobs 6 | { 7 | public class JobsSetting 8 | { 9 | [JsonProperty(PropertyName = "jobs", NullValueHandling = NullValueHandling.Include, 10 | Required = Required.AllowNull)] 11 | public IList Jobs { get; set; } 12 | 13 | [JsonProperty(PropertyName = "notifications", NullValueHandling = NullValueHandling.Ignore)] 14 | public IList Notifications { get; set; } // Notification on result events 15 | 16 | [JsonProperty(PropertyName = "notificationServices", NullValueHandling = NullValueHandling.Ignore)] 17 | public IList NotificationServices { get; set; } // Notification services data 18 | } 19 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Jobs/Recurring.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire; 3 | using NLog; 4 | 5 | namespace TaskScheduler.Jobs 6 | { 7 | /// 8 | /// Recurring jobs fire many times on the specified CRON schedule. as 9 | /// 10 | public class Recurring : Job 11 | { 12 | public override string Register() 13 | { 14 | try 15 | { 16 | JobId = $"{Name ?? ActionName}: {DateTime.Now.GetHashCode()}"; 17 | 18 | RecurringJob.AddOrUpdate(JobId, () => Trigger(this), TriggerOn, TimeZoneInfo.Local); 19 | 20 | Nlogger.Info($"The [{JobId}] added to hangfire recurring jobs successful."); 21 | 22 | return JobId; 23 | } 24 | catch (Exception exp) 25 | { 26 | Nlogger.Fatal(exp); 27 | return exp.Message; 28 | } 29 | } 30 | 31 | #region Properties 32 | 33 | private static readonly Logger Nlogger = LogManager.GetCurrentClassLogger(); 34 | 35 | public new JobType JobType 36 | { 37 | get => base.JobType; 38 | protected set => base.JobType = value; 39 | } 40 | 41 | #endregion 42 | 43 | #region Constructors 44 | 45 | public Recurring() 46 | { 47 | JobType = JobType.Recurring; 48 | } 49 | 50 | public Recurring(IJob job) : base(job) 51 | { 52 | JobType = JobType.Recurring; 53 | } 54 | 55 | #endregion 56 | } 57 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Jobs/SettingWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | using TaskScheduler.NotificationServices; 4 | 5 | namespace TaskScheduler.Jobs 6 | { 7 | public class SettingWrapper 8 | { 9 | [JsonProperty(PropertyName = "jobs", NullValueHandling = NullValueHandling.Include, 10 | Required = Required.AllowNull)] 11 | public IList Jobs { get; set; } 12 | 13 | [JsonProperty(PropertyName = "notifications", NullValueHandling = NullValueHandling.Ignore)] 14 | public IList Notifications { get; set; } // Notification on result events 15 | 16 | [JsonProperty(PropertyName = "notificationServices", NullValueHandling = NullValueHandling.Ignore)] 17 | public IList NotificationServices { get; set; } // Notification services data 18 | } 19 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NLog.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/CallRestApi/CallRestApiService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using TaskScheduler.Core; 8 | using TaskScheduler.Helper; 9 | 10 | namespace TaskScheduler.NotificationServices.CallRestApi 11 | { 12 | public class CallRestApiService : NotificationService 13 | { 14 | public new NotificationType NotificationType { get; } = NotificationType.CallRestApi; 15 | public string ClientId { get; set; } 16 | public string ClientSecret { get; set; } 17 | public string AuthServerUrl { get; set; } 18 | public string AuthGrantType { get; set; } 19 | public string AuthScope { get; set; } 20 | 21 | public CallRestApiService() { } 22 | 23 | public CallRestApiService(string clientId, string clientSecret, string authServerUrl, bool isDefaultService = false) 24 | { 25 | ClientId = clientId; 26 | ClientSecret = clientSecret; 27 | AuthServerUrl = authServerUrl; 28 | IsDefaultService = isDefaultService; 29 | } 30 | 31 | public override SystemNotification Send(string receiver, string message, string subject) 32 | { 33 | try 34 | { 35 | Logger.Info($"Notifying call rest api: {receiver}"); 36 | if (receiver.StartsWith("POST", true, CultureInfo.CurrentCulture)) 37 | { 38 | var authToken = GetAuthToken(); 39 | var url = GetValidUrl(receiver); 40 | var result = ActionRunner.CallRestApi( 41 | url, 42 | "POST", 43 | new Dictionary 44 | { 45 | {"cache-control", "no-cache"}, 46 | {"content-type", "application/x-www-form-urlencoded"}, 47 | {"taskScheduler-token", GetType().GUID.ToString()} 48 | }, 49 | new Dictionary 50 | { 51 | {"message", message}, 52 | {"subject", subject} 53 | }, 54 | new Dictionary 55 | { 56 | {"application/x-www-form-urlencoded", $"token={authToken}"} 57 | }, 58 | null); 59 | 60 | Logger.Info($"Post rest api successfully to: {url}"); 61 | return SystemNotification.SuccessfullyDone; 62 | } 63 | else // GET 64 | { 65 | var url = GetValidUrl(receiver); 66 | var result = ActionRunner.CallRestApi( 67 | url, 68 | "GET", 69 | new Dictionary 70 | { 71 | {"cache-control", "no-cache"}, 72 | {"taskScheduler-token", GetType().GUID.ToString()} 73 | }, 74 | new Dictionary 75 | { 76 | {"message", message}, 77 | {"subject", subject} 78 | }, 79 | null, 80 | null); 81 | 82 | Logger.Info($"Get rest api successfully to: {url}"); 83 | return SystemNotification.SuccessfullyDone; 84 | } 85 | } 86 | catch (Exception ex) 87 | { 88 | Logger.Fatal(ex, $"Notify call api failed for address: {receiver}"); 89 | return SystemNotification.InternalError; 90 | } 91 | } 92 | 93 | public override Task SendAsync(string receiver, string message, string subject) 94 | { 95 | throw new NotImplementedException(); 96 | } 97 | 98 | 99 | public string GetAuthToken() 100 | { 101 | try 102 | { 103 | var authHead = $"{ClientId}:{ClientSecret}".EncodeToBase64(); 104 | 105 | var token = ActionRunner.CallRestApi( 106 | AuthServerUrl, 107 | "POST", 108 | new Dictionary 109 | { 110 | {"cache-control", "no-cache"}, 111 | {"authorization", $"Basic {authHead}"}, 112 | {"content-type", "application/x-www-form-urlencoded"}, 113 | {"taskScheduler-token", GetType().GUID.ToString()} 114 | }, 115 | null, 116 | new Dictionary 117 | { 118 | {"application/x-www-form-urlencoded", $"grant_type={AuthGrantType}&scope={AuthScope}"} 119 | }, 120 | null); 121 | 122 | var tokenKey = "access_token"; 123 | if (token?.Contains(tokenKey) != true) return token; 124 | var iAccTok = token.IndexOf("{", StringComparison.OrdinalIgnoreCase); 125 | token = token.Substring(iAccTok, token.Length - iAccTok); 126 | token = token.Trim(); 127 | var tokenObj = (JObject)JsonConvert.DeserializeObject(token); 128 | 129 | return tokenObj[tokenKey].ToString(Formatting.None).Replace("\"", ""); 130 | } 131 | catch (Exception ex) 132 | { 133 | Logger.Fatal(ex, $"Can not give any authentication token from {AuthServerUrl}"); 134 | return null; 135 | } 136 | } 137 | 138 | private string GetValidUrl(string url) 139 | { 140 | url = url?.ToLower(); 141 | 142 | if (string.IsNullOrEmpty(url)) throw new ArgumentNullException(nameof(url)); 143 | if (!url.Contains("http")) return "http://" + url; 144 | if (url.StartsWith("http")) return url; 145 | 146 | var indexOfHttp = url.LastIndexOf("http", StringComparison.OrdinalIgnoreCase); 147 | return url.Substring(indexOfHttp, url.Length - indexOfHttp); 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/Email/EmailService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Net; 5 | using System.Net.Mail; 6 | using System.Net.Mime; 7 | using System.Reflection; 8 | using System.Text; 9 | using System.Text.RegularExpressions; 10 | using System.Threading.Tasks; 11 | using TaskScheduler.Helper; 12 | 13 | namespace TaskScheduler.NotificationServices.Email 14 | { 15 | public class EmailService : NotificationService 16 | { 17 | public new NotificationType NotificationType { get; } = NotificationType.Email; 18 | public string SmtpPassword { get; set; } 19 | public string SmtpSenderEmail { get; set; } 20 | public string SmtpUrl { get; set; } 21 | public int SmtpPort { get; set; } 22 | public string Logo { get; set; } 23 | public string LogoUrl { get; set; } 24 | public string ClickUrl { get; set; } 25 | public string UnsubscribeUrl { get; set; } 26 | protected SmtpClient Client { get; set; } 27 | 28 | public EmailService() { } 29 | public EmailService(string smtpUrl, string smtpPassword, int smtpPort, string smtpSenderEmail, 30 | string clickUrl = "#", string unsubscribeUrl = "#", string logo = null, string logoUrl = null, bool isDefaultService = false) 31 | { 32 | SmtpUrl = smtpUrl; 33 | SmtpPassword = smtpPassword; 34 | SmtpPort = smtpPort; 35 | SmtpSenderEmail = smtpSenderEmail; 36 | ClickUrl = clickUrl; 37 | UnsubscribeUrl = unsubscribeUrl; 38 | Logo = logo; 39 | LogoUrl = logoUrl; 40 | IsDefaultService = isDefaultService; 41 | Initial(); 42 | } 43 | 44 | public sealed override void Initial() 45 | { 46 | if (Client == null) 47 | { 48 | Client = GetSmtpClient(); 49 | } 50 | } 51 | 52 | public override SystemNotification Send(string receiver, string message, string subject) 53 | { 54 | var completed = true; 55 | if (string.IsNullOrEmpty(receiver)) 56 | return SystemNotification.InvalidOperation; 57 | 58 | foreach (var email in receiver.SplitUp()) 59 | try 60 | { 61 | if (string.IsNullOrEmpty(email) || !ValidateEmailAddressSyntax(email)) 62 | { 63 | Logger.Warn($"The {email} email address is not valid!"); 64 | completed = false; 65 | } 66 | 67 | var mailMessage = GetMailMessage(email, message.CleanText(), subject.CleanText()); 68 | 69 | using (var smtpClient = GetSmtpClient()) 70 | { 71 | smtpClient.Send(mailMessage); 72 | Logger.Info($"Email Send successfully to: {email}"); 73 | } 74 | } 75 | catch (Exception ex) 76 | { 77 | Logger.Fatal(ex, $"Send email failed for sending mail to {email}"); 78 | completed = false; 79 | } 80 | 81 | return completed ? SystemNotification.SuccessfullyDone : SystemNotification.InternalError; 82 | } 83 | 84 | public override async Task SendAsync(string receiver, string message, string subject) 85 | { 86 | var completed = true; 87 | if (string.IsNullOrEmpty(receiver)) 88 | return SystemNotification.InvalidOperation; 89 | 90 | foreach (var email in receiver.SplitUp()) 91 | try 92 | { 93 | if (string.IsNullOrEmpty(email) || !ValidateEmailAddressSyntax(email)) 94 | { 95 | Logger.Warn($"The {email} email address is not valid!"); 96 | completed = false; 97 | } 98 | 99 | var mailMessage = GetMailMessage(email, message.CleanText(), subject.CleanText()); 100 | 101 | using (var smtpClient = GetSmtpClient()) 102 | { 103 | await smtpClient.SendMailAsync(mailMessage); 104 | Logger.Info($"Email Send successfully to: {email}"); 105 | } 106 | } 107 | catch (Exception ex) 108 | { 109 | Logger.Fatal(ex, $"Send email failed for sending mail to {email}"); 110 | completed = false; 111 | } 112 | 113 | return completed ? SystemNotification.SuccessfullyDone : SystemNotification.InternalError; 114 | } 115 | 116 | public SmtpClient GetSmtpClient() 117 | { 118 | var smtpClient = new SmtpClient 119 | { 120 | Host = SmtpUrl, 121 | Port = SmtpPort, 122 | DeliveryMethod = SmtpDeliveryMethod.Network, 123 | UseDefaultCredentials = false 124 | }; 125 | if (SmtpSenderEmail != null) 126 | smtpClient.Credentials = 127 | new NetworkCredential(SmtpSenderEmail, SmtpPassword); 128 | 129 | return smtpClient; 130 | } 131 | 132 | private MailMessage GetMailMessage(string receiver, string message, string subject) 133 | { 134 | // Construct the alternate body as HTML. 135 | var body = FileManager.ReadResourceFile("NotificationServices.Email.EmailTemplate.html"); 136 | 137 | body = body 138 | ?.Replace("{message}", message?.Replace("\n", "").Replace("\t", " ")) 139 | .Replace("{subject}", subject.ToLowerCaseNamingConvention()) 140 | .Replace("{title}", Assembly.GetEntryAssembly().GetName().Name.ToLowerCaseNamingConvention()) 141 | .Replace("{unsubscribeUrl}", UnsubscribeUrl) 142 | .Replace("{clickUrl}", ClickUrl) 143 | .Replace("{receiver}", receiver.Replace(".", "_")) 144 | .Replace("{version}", Assembly.GetEntryAssembly().GetName().Version.ToString(3)) 145 | .Replace("{emailDateTime}", DateTime.Now.ToString("F", CultureInfo.CreateSpecificCulture("en"))); 146 | 147 | var mail = new MailMessage(SmtpSenderEmail, receiver) 148 | { 149 | SubjectEncoding = Encoding.UTF8, 150 | BodyEncoding = Encoding.UTF8, 151 | From = new MailAddress(SmtpSenderEmail), 152 | To = { receiver }, 153 | Subject = $"{subject} - {DateTime.Now.ToString("g", CultureInfo.CreateSpecificCulture("en"))}", 154 | Body = body ?? message.Replace(@"\n", ""), 155 | IsBodyHtml = true, 156 | Priority = MailPriority.High 157 | }; 158 | 159 | if (Logo != null) 160 | { 161 | var imgStream = new MemoryStream(Convert.FromBase64String(Logo)); 162 | var attachment = new Attachment(imgStream, "logo.png") 163 | { 164 | ContentId = "logo.png", 165 | TransferEncoding = TransferEncoding.Base64 166 | }; 167 | mail.Attachments.Add(attachment); 168 | } 169 | else if (LogoUrl != null) 170 | { 171 | mail.Body = mail.Body.Replace("cid:logo.png", LogoUrl); 172 | } 173 | 174 | return mail; 175 | } 176 | 177 | /// 178 | /// Verifies if the specified string is a correctly formatted e-mail address. 179 | /// 180 | /// The e-mail address string. 181 | /// true if the syntax of the specified string is correct; otherwise, false. 182 | /// 183 | /// This method only checks the syntax of the e-mail address. It does NOT make any network connections and thus 184 | /// does NOT actually check if this address exists and you can send e-mails there. 185 | /// 186 | /// As this method is static, you do not need to create an instance of 187 | /// object in order to use it. 188 | /// 189 | /// 190 | /// 191 | /// This example returns Correct syntax as _mike.o'neil_loves_underscores@sub-domain.travel address is 192 | /// complex but still has valid syntax. 193 | /// 194 | /// using NotificationServices.EmailService; 195 | /// 196 | /// if (EmailService.ValidateEmailAddressSyntax("_mike.o'neil_loves_underscores@sub-domain.travel")) 197 | /// { 198 | /// Console.WriteLine("Correct syntax"); 199 | /// } 200 | /// else 201 | /// { 202 | /// Console.WriteLine("Wrong syntax"); 203 | /// } 204 | /// 205 | /// 206 | public static bool ValidateEmailAddressSyntax(string email) 207 | { 208 | if (email == null) 209 | throw new ArgumentNullException(nameof(email)); 210 | 211 | return Regex.IsMatch(email, 212 | "^(([\\w]+['\\.\\-+])+[\\w]+|([\\w]+))@((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])){1}|([a-zA-Z0-9]+[\\w-]*\\.)+[a-zA-Z]{2,9})$"); 213 | } 214 | } 215 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/Email/EmailTemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Task Scheduler Notify Message v{version} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | {title} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {subject} 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {message} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | Sent from Task Scheduler Notify API at {emailDateTime} 84 | 85 | 86 | 87 | 88 | If you do not want receive these emails, please click on 89 | 91 | Unsubscribe 92 | . 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/INotificationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | 5 | namespace TaskScheduler.NotificationServices 6 | { 7 | public interface INotificationService 8 | { 9 | [JsonConverter(typeof(StringEnumConverter))] 10 | [JsonProperty(PropertyName = "notificationType", NullValueHandling = NullValueHandling.Include)] 11 | NotificationType NotificationType { get; set; } 12 | 13 | [JsonProperty(PropertyName = "serviceName", NullValueHandling = NullValueHandling.Include)] 14 | string ServiceName { get; set; } 15 | 16 | // is default service for the special notification type's 17 | [JsonProperty(PropertyName = "isDefaultService", NullValueHandling = NullValueHandling.Ignore)] 18 | bool IsDefaultService { get; set; } 19 | 20 | SystemNotification Send(string receiver, string message, string subject); 21 | 22 | Task SendAsync(string receiver, string message, string subject); 23 | void Initial(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/NotificaionType.cs: -------------------------------------------------------------------------------- 1 | namespace TaskScheduler.NotificationServices 2 | { 3 | public enum NotificationType 4 | { 5 | Email, 6 | Sms, 7 | Telegram, 8 | Slack, 9 | CallRestApi 10 | } 11 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/Notification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using TaskScheduler.Core; 7 | 8 | namespace TaskScheduler.NotificationServices 9 | { 10 | public class Notification : IEquatable, IEqualityComparer, ICloneable 11 | { 12 | [JsonProperty(NullValueHandling = NullValueHandling.Include)] 13 | public string NotificationServiceName { get; set; } 14 | 15 | [JsonProperty(NullValueHandling = NullValueHandling.Include)] 16 | public string Receiver { get; set; } 17 | 18 | public object Clone() 19 | { 20 | return new Notification 21 | { 22 | NotificationServiceName = NotificationServiceName, 23 | Receiver = Receiver 24 | }; 25 | } 26 | 27 | public bool Equals(Notification x, Notification y) 28 | { 29 | return x?.Equals(y) == true; 30 | } 31 | 32 | public int GetHashCode(Notification obj) 33 | { 34 | return obj.GetHashCode(); 35 | } 36 | 37 | 38 | public bool Equals(Notification other) 39 | { 40 | return NotificationServiceName == other?.NotificationServiceName && 41 | Receiver == other?.Receiver; 42 | } 43 | 44 | 45 | public void Notify(string message, string subject) 46 | { 47 | var service = 48 | JobsManager.Setting.NotificationServices.FirstOrDefault(ns => 49 | ns.ServiceName.Equals(NotificationServiceName, StringComparison.OrdinalIgnoreCase)); 50 | 51 | var ver = GetType().Assembly.GetName().Version.ToString(3); 52 | subject = subject.Replace("{version}", ver); 53 | message = message.Replace("{version}", ver); 54 | service?.Send(Receiver, message, subject); 55 | } 56 | 57 | public async Task NotifyAsync(string message, string subject) 58 | { 59 | var service = GetNotificationService(); 60 | if (service != null) 61 | { 62 | var ver = GetType().Assembly.GetName().Version.ToString(3); 63 | subject = subject.Replace("{version}", ver); 64 | message = message.Replace("{version}", ver); 65 | await service.SendAsync(Receiver, message, subject); 66 | } 67 | } 68 | 69 | public INotificationService GetNotificationService() 70 | { 71 | return 72 | JobsManager.Setting.NotificationServices.FirstOrDefault(ns => 73 | ns.ServiceName.Equals(NotificationServiceName, StringComparison.OrdinalIgnoreCase)); 74 | } 75 | 76 | 77 | public override int GetHashCode() 78 | { 79 | if (string.IsNullOrWhiteSpace(NotificationServiceName) || string.IsNullOrWhiteSpace(Receiver)) 80 | return 0; 81 | 82 | return NotificationServiceName.GetHashCode() ^ Receiver.GetHashCode(); 83 | } 84 | 85 | public override bool Equals(object obj) 86 | { 87 | var notifyObj = obj as Notification; 88 | 89 | return notifyObj != null && Equals(notifyObj); 90 | } 91 | 92 | public override string ToString() 93 | { 94 | return JsonConvert.SerializeObject(this, Formatting.Indented); 95 | } 96 | 97 | public static bool operator ==(Notification notifyA, Notification notifyB) 98 | { 99 | return notifyA?.Equals(notifyB) ?? ReferenceEquals(notifyB, null); 100 | } 101 | 102 | public static bool operator !=(Notification notifyA, Notification notifyB) 103 | { 104 | return !(notifyA == notifyB); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/NotificationService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using NLog; 3 | 4 | namespace TaskScheduler.NotificationServices 5 | { 6 | public abstract class NotificationService : INotificationService 7 | { 8 | protected ILogger Logger { get; set; } 9 | public NotificationType NotificationType { get; set; } 10 | public string ServiceName { get; set; } 11 | public bool IsDefaultService { get; set; } 12 | 13 | protected NotificationService() 14 | { 15 | Logger = LogManager.GetCurrentClassLogger(); 16 | } 17 | 18 | 19 | public abstract SystemNotification Send(string receiver, string message, string subject); 20 | public abstract Task SendAsync(string receiver, string message, string subject); 21 | public virtual void Initial() { } 22 | } 23 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/NotifyCondition.cs: -------------------------------------------------------------------------------- 1 | namespace TaskScheduler.NotificationServices 2 | { 3 | public enum NotifyCondition 4 | { 5 | None = 0, 6 | Equals, 7 | NotEquals, 8 | MoreThan, 9 | LessThan, 10 | EqualsOrMoreThan, 11 | EqualsOrLessThan 12 | } 13 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/SMS/CellphoneNumber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using NLog; 6 | 7 | namespace TaskScheduler.NotificationServices.SMS 8 | { 9 | public class CellphoneNumber 10 | { 11 | private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); 12 | 13 | private static readonly ConcurrentDictionary> SendHistory = 14 | new ConcurrentDictionary>(); 15 | 16 | public CellphoneNumber(string phoneNumber) 17 | { 18 | phoneNumber = phoneNumber.Trim(); 19 | 20 | long tel; 21 | if (!long.TryParse(phoneNumber, out tel)) 22 | throw new Exception("phone number should just contain numbers"); 23 | 24 | if (phoneNumber.StartsWith("+98")) 25 | phoneNumber = "0" + phoneNumber.Substring(3); 26 | else if (phoneNumber.StartsWith("98")) 27 | phoneNumber = "0" + phoneNumber.Substring(2); 28 | else if (phoneNumber.StartsWith("0098")) 29 | phoneNumber = "0" + phoneNumber.Substring(4); 30 | else if (!phoneNumber.StartsWith("0")) 31 | phoneNumber = "0" + phoneNumber; 32 | 33 | if (phoneNumber.Length != 11) 34 | throw new Exception("phone number length is not 11"); 35 | 36 | if (!phoneNumber.StartsWith("09")) 37 | throw new Exception("phone number dos not start with 09"); 38 | 39 | PhoneNumber = phoneNumber; 40 | } 41 | 42 | public string PhoneNumber { get; } 43 | 44 | public bool IsSendingLimitExceeded() 45 | { 46 | List history; 47 | if (SendHistory.ContainsKey(PhoneNumber)) 48 | { 49 | history = SendHistory[PhoneNumber]; 50 | } 51 | else 52 | { 53 | history = new List(); 54 | SendHistory[PhoneNumber] = history; 55 | } 56 | 57 | if (history.Count(d => d > DateTime.Now.AddMinutes(-2)) >= 3) 58 | { 59 | Logger.Warn($"MinuteSendingLimitExceeded for {PhoneNumber}"); 60 | return true; 61 | } 62 | 63 | if (history.Count(d => d > DateTime.Now.AddHours(-1)) >= 6) 64 | { 65 | Logger.Warn($"HourSendingLimitExceeded for {PhoneNumber}"); 66 | return true; 67 | } 68 | 69 | if (history.Count(d => d > DateTime.Now.AddDays(-1)) >= 20) 70 | { 71 | Logger.Warn($"DaySendingLimitExceeded for {PhoneNumber}"); 72 | return true; 73 | } 74 | 75 | history.Add(DateTime.Now); 76 | return false; 77 | } 78 | 79 | public static CellphoneNumber Normalize(string number) 80 | { 81 | CellphoneNumber cellphone; 82 | try 83 | { 84 | cellphone = new CellphoneNumber(number); 85 | } 86 | catch (Exception exp) 87 | { 88 | Logger.Warn($"Invalid send SMS request received. {exp.Message}"); 89 | return null; 90 | } 91 | 92 | return cellphone; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/SMS/RahyabSmsService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using RestSharp; 3 | 4 | namespace TaskScheduler.NotificationServices.SMS 5 | { 6 | public class RahyabSmsService 7 | { 8 | private readonly RestClient _client; 9 | private readonly string _password; 10 | private readonly string _sendNumber; 11 | private readonly string _userName; 12 | 13 | public RahyabSmsService(string userName, string password, string sendNumber) 14 | { 15 | _client = new RestClient("http://linepayamak.ir/url/post"); 16 | _userName = userName; 17 | _password = password; 18 | _sendNumber = sendNumber; 19 | } 20 | 21 | public void SendSms(string receiveNumber, string message) 22 | { 23 | var request = new RestRequest("SendSMS.ashx", Method.POST); 24 | request.AddParameter("from", _sendNumber); 25 | request.AddParameter("to", receiveNumber); 26 | request.AddParameter("text", message); 27 | request.AddParameter("password", _password); 28 | request.AddParameter("username", _userName); 29 | _client.Execute(request); 30 | } 31 | 32 | public async Task SendSmsAsync(string receiveNumber, string message) 33 | { 34 | var request = new RestRequest("SendSMS.ashx", Method.POST); 35 | request.AddParameter("from", _sendNumber); 36 | request.AddParameter("to", receiveNumber); 37 | request.AddParameter("text", message); 38 | request.AddParameter("password", _password); 39 | request.AddParameter("username", _userName); 40 | await _client.ExecuteTaskAsync(request); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/SMS/SmsService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using TaskScheduler.Helper; 5 | 6 | namespace TaskScheduler.NotificationServices.SMS 7 | { 8 | public class SmsService : NotificationService 9 | { 10 | public new NotificationType NotificationType { get; } = NotificationType.Sms; 11 | public string Username { get; set; } 12 | public string Password { get; set; } 13 | public string SenderNumber { get; set; } 14 | 15 | public SmsService() { } 16 | 17 | public SmsService(string username, string password, string sender, bool isDefaultService = false) 18 | { 19 | Username = username; 20 | Password = password; 21 | SenderNumber = sender; 22 | IsDefaultService = isDefaultService; 23 | Initial(); 24 | } 25 | 26 | public sealed override void Initial() 27 | { 28 | if (SenderNumber.Any(d => !(char.IsDigit(d) || d == '+'))) 29 | throw new ArgumentException(SenderNumber, "The sender number must be positive numbers!"); 30 | } 31 | 32 | public override SystemNotification Send(string receiver, string message, string subject) 33 | { 34 | var completed = true; 35 | if (string.IsNullOrEmpty(receiver)) return SystemNotification.InvalidOperation; 36 | var service = new RahyabSmsService(Username, Password, SenderNumber); 37 | foreach (var phone in receiver.Split(new[] { ",", ";", " " }, StringSplitOptions.RemoveEmptyEntries)) 38 | { 39 | var cellphone = CellphoneNumber.Normalize(phone); 40 | 41 | try 42 | { 43 | if (cellphone == null) throw new Exception($"Invalid Phone: {phone}"); 44 | service.SendSms(cellphone.PhoneNumber, subject.CleanText() + "\n\n" + message.CleanText()); 45 | Logger.Info($"SMS Sent successfully to: {cellphone.PhoneNumber}"); 46 | } 47 | catch (Exception ex) 48 | { 49 | Logger.Fatal(ex, $"Send SMS failed for sending message to {phone}"); 50 | completed = false; 51 | } 52 | } 53 | 54 | return completed ? SystemNotification.SuccessfullyDone : SystemNotification.InternalError; 55 | } 56 | 57 | public override async Task SendAsync(string receiver, string message, string subject) 58 | { 59 | var completed = true; 60 | if (string.IsNullOrEmpty(receiver)) return SystemNotification.InvalidOperation; 61 | var service = new RahyabSmsService(Username, Password, SenderNumber); 62 | foreach (var phone in receiver.Split(new[] { ",", ";", " " }, StringSplitOptions.RemoveEmptyEntries)) 63 | { 64 | var cellphone = CellphoneNumber.Normalize(phone); 65 | 66 | try 67 | { 68 | if (cellphone == null) throw new Exception($"Invalid Phone: {phone}"); 69 | await service.SendSmsAsync(cellphone.PhoneNumber, subject.CleanText() + "\n\n" + message.CleanText()); 70 | Logger.Info($"SMS Sent successfully to: {cellphone.PhoneNumber}"); 71 | } 72 | catch (Exception ex) 73 | { 74 | Logger.Fatal(ex, $"Send SMS failed for sending message to {phone}"); 75 | completed = false; 76 | } 77 | } 78 | 79 | return completed ? SystemNotification.SuccessfullyDone : SystemNotification.InternalError; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/Slack/SlackService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using SlackMessenger; 5 | using TaskScheduler.Helper; 6 | 7 | namespace TaskScheduler.NotificationServices.Slack 8 | { 9 | public class SlackService : NotificationService 10 | { 11 | protected SlackClient Client { get; set; } 12 | public new NotificationType NotificationType { get; } = NotificationType.Slack; 13 | public string SenderName { get; set; } 14 | public string WebhookUrl { get; set; } 15 | public string IconUrl { get; set; } 16 | 17 | public SlackService() { } 18 | public SlackService(string sender, string webhookUrl, string icon, bool isDefaultService = false) 19 | { 20 | SenderName = sender; 21 | WebhookUrl = webhookUrl; 22 | IconUrl = icon; 23 | IsDefaultService = isDefaultService; 24 | Initial(); 25 | } 26 | 27 | public sealed override void Initial() 28 | { 29 | if (Client == null) 30 | Client = new SlackClient(WebhookUrl); 31 | } 32 | 33 | public override SystemNotification Send(string receiver, string message, string subject) 34 | { 35 | var completed = true; 36 | if (string.IsNullOrEmpty(receiver)) 37 | return SystemNotification.InvalidOperation; 38 | 39 | foreach (var id in receiver.SplitUp()) 40 | if (!SendSingleSlackMessage(id, message, subject)) 41 | completed = false; 42 | 43 | return completed 44 | ? SystemNotification.SuccessfullyDone 45 | : SystemNotification.InternalError; 46 | } 47 | 48 | public override async Task SendAsync(string receiver, string message, string subject) 49 | { 50 | var completed = true; 51 | if (string.IsNullOrEmpty(receiver)) return SystemNotification.InvalidOperation; 52 | var tasks = new List(); 53 | 54 | foreach (var id in receiver.SplitUp()) 55 | tasks.Add(new Task(() => 56 | { 57 | if (!SendSingleSlackMessage(id, message, subject)) 58 | completed = false; 59 | })); 60 | await Task.WhenAll(tasks.ToArray()); 61 | 62 | return completed 63 | ? SystemNotification.SuccessfullyDone 64 | : SystemNotification.InternalError; 65 | } 66 | 67 | protected bool SendSingleSlackMessage(string receiver, string message, string subject) 68 | { 69 | try 70 | { 71 | subject = subject.Replace("**", "*").Replace("\\", "/"); 72 | message = message.Replace("**", "*").Replace("\\", "/"); 73 | Logger.Info($"Sending slack to #{receiver} channel ..."); 74 | var msg = new Message($"{subject} \n\n {message}", receiver, SenderName, IconUrl); 75 | Client.Send(msg); 76 | Logger.Info($"Slack message sent to #{receiver} channel successful."); 77 | return true; 78 | } 79 | catch (Exception ex) 80 | { 81 | Logger.Fatal(ex, $"Send telegram failed for telegram id: {receiver}"); 82 | return false; 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/Status.cs: -------------------------------------------------------------------------------- 1 | namespace TaskScheduler.NotificationServices 2 | { 3 | public enum Status 4 | { 5 | Successful = 0, 6 | InternalError = 1, 7 | InvalidSession = 2, 8 | InputError = 3, 9 | LoginNeeded = 4, 10 | DuplicateEmail = 5, 11 | BadContract = 6, 12 | RedirectToPayment = 7, 13 | ForceUpdate = 8, 14 | ForceLogout = 9, 15 | InvokeKeepUpdate = 10 16 | } 17 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/SystemNotification.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TaskScheduler.NotificationServices 4 | { 5 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 6 | public class SystemNotification 7 | { 8 | public static readonly SystemNotification SuccessfullyDone = new SystemNotification 9 | { 10 | Status = Status.Successful, 11 | Message = null, 12 | AdditionalData = "The operation successful completed." 13 | }; 14 | 15 | public static readonly SystemNotification InternalError = new SystemNotification 16 | { 17 | Status = Status.BadContract, 18 | Message = "One error occurred in system, Please try again.", 19 | AdditionalData = null 20 | }; 21 | 22 | public static readonly SystemNotification InvalidOperation = new SystemNotification 23 | { 24 | Status = Status.BadContract, 25 | Message = "Your request is invalid!", 26 | AdditionalData = null 27 | }; 28 | 29 | public static readonly SystemNotification TooMuchRecipients = new SystemNotification 30 | { 31 | Status = Status.BadContract, 32 | Message = "You can't send email to more than 999 people.", 33 | AdditionalData = null 34 | }; 35 | 36 | public static readonly SystemNotification SendingLimitExceeded = new SystemNotification 37 | { 38 | Status = Status.BadContract, 39 | Message = "Please try again later.", 40 | AdditionalData = null 41 | }; 42 | 43 | 44 | [JsonProperty("additionalData")] public string AdditionalData { get; set; } 45 | 46 | [JsonProperty("message")] public string Message { get; set; } 47 | 48 | [JsonProperty("status")] public Status Status { get; set; } 49 | } 50 | } -------------------------------------------------------------------------------- /src/TaskScheduler/NotificationServices/Telegram/TelegramService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using TaskScheduler.Helper; 5 | using Telegram.Bot; 6 | using Telegram.Bot.Args; 7 | using Telegram.Bot.Types.Enums; 8 | 9 | namespace TaskScheduler.NotificationServices.Telegram 10 | { 11 | public class TelegramService : NotificationService 12 | { 13 | protected TelegramBotClient Bot { get; set; } 14 | 15 | public new NotificationType NotificationType { get; } = NotificationType.Telegram; 16 | public string Username { get; set; } 17 | public string ApiKey { get; set; } 18 | public string SenderBot { get; set; } 19 | 20 | public TelegramService() { } 21 | public TelegramService(string username, string apiKey, string senderBot, bool isDefaultService = false) 22 | { 23 | Username = username; 24 | ApiKey = apiKey; 25 | SenderBot = senderBot; 26 | IsDefaultService = isDefaultService; 27 | Initial(); 28 | } 29 | 30 | public sealed override void Initial() 31 | { 32 | if (Bot == null) 33 | { 34 | Logger.Info("Running telegram bot..."); 35 | Bot = new TelegramBotClient(ApiKey); 36 | Bot.StartReceiving(); 37 | Bot.OnMessage += Bot_OnMessage; 38 | 39 | var me = Task.Run(async () => await Bot.GetMeAsync()).Result; 40 | Logger.Info($"The {me.Username} bot is running."); 41 | } 42 | } 43 | 44 | public override SystemNotification Send(string receiver, string message, string subject) 45 | { 46 | var completed = true; 47 | if (string.IsNullOrEmpty(receiver)) 48 | return SystemNotification.InvalidOperation; 49 | 50 | Parallel.ForEach(receiver.SplitUp(), id => 51 | { 52 | try 53 | { 54 | Bot.SendTextMessageAsync(id.Trim(), $"{subject} \n {message}", parseMode: ParseMode.Markdown) 55 | .ContinueWith(result => Logger.Info($"Telegram message sent to {id} id successful.")); 56 | } 57 | catch (Exception ex) 58 | { 59 | Logger.Fatal(ex, $"Send telegram failed for telegram id: {id}"); 60 | completed = false; 61 | } 62 | }); 63 | 64 | return completed 65 | ? SystemNotification.SuccessfullyDone 66 | : SystemNotification.InternalError; 67 | } 68 | 69 | public override async Task SendAsync(string receiver, string message, string subject) 70 | { 71 | if (string.IsNullOrEmpty(receiver)) return SystemNotification.InvalidOperation; 72 | var completed = true; 73 | var tasks = new List(); 74 | var ids = receiver.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); 75 | foreach (var id in ids) 76 | { 77 | try 78 | { 79 | Logger.Info($"Sending telegram to id: {id} ..."); 80 | tasks.Add(Bot.SendTextMessageAsync(id.Trim(), $"{subject} \n {message}", 81 | parseMode: ParseMode.Markdown)); 82 | Logger.Info($"Telegram message sent to {id} id successful."); 83 | } 84 | catch (Exception ex) 85 | { 86 | completed = false; 87 | Logger.Fatal(ex, $"Send telegram failed for telegram id: {id}"); 88 | } 89 | } 90 | 91 | await Task.WhenAll(tasks.ToArray()); 92 | 93 | return completed ? SystemNotification.SuccessfullyDone : SystemNotification.InternalError; 94 | } 95 | 96 | protected void Bot_OnMessage(object sender, MessageEventArgs e) 97 | { 98 | Logger.Info($"The {e.Message.From.Username} user call notification bot by message: {e.Message.Text}"); 99 | Bot.SendTextMessageAsync(e.Message.From.Id, e.Message.From.Id.ToString()); 100 | Logger.Info($"Response to user by user id: {e.Message.From.Id}"); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using NLog; 5 | using Topshelf; 6 | using AssemblyInfo = TaskScheduler.Helper.AssemblyInfo; 7 | 8 | namespace TaskScheduler 9 | { 10 | public class Program 11 | { 12 | private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); 13 | 14 | // ReSharper disable once UnusedParameter.Local 15 | private static void Main(string[] args) 16 | { 17 | Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.AboveNormal; 18 | Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; 19 | 20 | var rc = HostFactory.Run(configurator => 21 | { 22 | configurator.UseNLog(Logger.Factory); 23 | configurator.RunAsLocalSystem(); 24 | configurator.SetDisplayName( 25 | $"{AssemblyInfo.Company} {AssemblyInfo.Title} v{AssemblyInfo.Version.ToString(3)}"); 26 | configurator.SetDescription(AssemblyInfo.Description); 27 | configurator.SetServiceName(AssemblyInfo.Product); 28 | configurator.SetStartTimeout(TimeSpan.FromMinutes(5)); 29 | configurator.OnException(exception => 30 | { 31 | LogManager.GetCurrentClassLogger().Fatal(exception); 32 | Debugger.Break(); 33 | }); 34 | configurator.Service(serviceConfigurator => 35 | { 36 | serviceConfigurator.ConstructUsing(settings => new Service(settings, Properties.Settings.Default.Port)); 37 | serviceConfigurator.WhenStarted(service => service.Start()); 38 | serviceConfigurator.WhenStopped(service => service.Stop()); 39 | }); 40 | 41 | configurator.EnableServiceRecovery(r => 42 | { 43 | //you can have up to three of these 44 | r.RestartService(1); 45 | 46 | //should this be true for crashed or non-zero exits 47 | r.OnCrashOnly(); 48 | 49 | //number of days until the error count resets 50 | //r.SetResetPeriod(1); // set the reset interval to one day 51 | }); 52 | 53 | configurator.StartAutomatically(); // only executed if the service is being installed. 54 | }); 55 | if (rc == TopshelfExitCode.Ok) 56 | Logger.Info("Exiting with success code"); 57 | else 58 | Logger.Error($"Exiting with failure code: {rc}"); 59 | 60 | var exitCode = Convert.ChangeType(rc, rc.GetTypeCode()) as int?; 61 | Environment.ExitCode = exitCode ?? 1; 62 | 63 | Console.ReadKey(); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Task Scheduler")] 8 | [assembly: AssemblyDescription("Manage and execute actions by Hangfire job runners.")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("bezzad")] 11 | [assembly: AssemblyProduct("TaskScheduler")] 12 | [assembly: AssemblyCopyright("Copyright © 2017-2019 Behzad Khosravifar")] 13 | [assembly: AssemblyTrademark("bezzad")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("a0a1bfe1-d843-45db-8884-7c0ea1351670")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.8.1")] 35 | [assembly: AssemblyFileVersion("1.8.1")] -------------------------------------------------------------------------------- /src/TaskScheduler/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TaskScheduler.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.8.0.0")] 16 | public sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.ApplicationScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("TaskSchedulerSetting.json")] 29 | public string SettingFileName { 30 | get { 31 | return ((string)(this["SettingFileName"])); 32 | } 33 | } 34 | 35 | [global::System.Configuration.ApplicationScopedSettingAttribute()] 36 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 37 | [global::System.Configuration.DefaultSettingValueAttribute("App_Data.installHangfireDatabase.sql")] 38 | public string HangfireDbScript { 39 | get { 40 | return ((string)(this["HangfireDbScript"])); 41 | } 42 | } 43 | 44 | [global::System.Configuration.ApplicationScopedSettingAttribute()] 45 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 46 | [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.ConnectionString)] 47 | [global::System.Configuration.DefaultSettingValueAttribute("Server=.;Persist Security Info=False;Integrated Security=true;Initial Catalog=Tas" + 48 | "k;")] 49 | public string LocalConnectionString { 50 | get { 51 | return ((string)(this["LocalConnectionString"])); 52 | } 53 | } 54 | 55 | [global::System.Configuration.ApplicationScopedSettingAttribute()] 56 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 57 | [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.ConnectionString)] 58 | [global::System.Configuration.DefaultSettingValueAttribute("Server=.;Persist Security Info=False;Integrated Security=true;Initial Catalog=Tas" + 59 | "k;")] 60 | public string ServerConnectionString { 61 | get { 62 | return ((string)(this["ServerConnectionString"])); 63 | } 64 | } 65 | 66 | [global::System.Configuration.ApplicationScopedSettingAttribute()] 67 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 68 | [global::System.Configuration.DefaultSettingValueAttribute("8002")] 69 | public int Port { 70 | get { 71 | return ((int)(this["Port"])); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/TaskScheduler/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TaskSchedulerSetting.json 7 | 8 | 9 | App_Data.installHangfireDatabase.sql 10 | 11 | 12 | <?xml version="1.0" encoding="utf-16"?> 13 | <SerializableConnectionString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 14 | <ConnectionString>Server=.;Persist Security Info=False;Integrated Security=true;Initial Catalog=Task;</ConnectionString> 15 | </SerializableConnectionString> 16 | Server=.;Persist Security Info=False;Integrated Security=true;Initial Catalog=Task; 17 | 18 | 19 | <?xml version="1.0" encoding="utf-16"?> 20 | <SerializableConnectionString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 21 | <ConnectionString>Server=.;Persist Security Info=False;Integrated Security=true;Initial Catalog=Task;</ConnectionString> 22 | </SerializableConnectionString> 23 | Server=.;Persist Security Info=False;Integrated Security=true;Initial Catalog=Task; 24 | 25 | 26 | 8002 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/TaskScheduler/Service.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net; 4 | using System.Threading; 5 | using Microsoft.Owin.Hosting; 6 | using NLog; 7 | using Topshelf.Runtime; 8 | 9 | namespace TaskScheduler 10 | { 11 | public class Service 12 | { 13 | private readonly ILogger _logger; 14 | private readonly int _port; 15 | private Thread _service; 16 | private HostSettings _settings; 17 | private IDisposable _webServer; 18 | 19 | public Service(HostSettings settings, int port) 20 | { 21 | _logger = LogManager.GetCurrentClassLogger(); 22 | _settings = settings; 23 | _port = port; 24 | } 25 | 26 | public bool Start() 27 | { 28 | var header = @" 29 | _____ _ 30 | |_ _| | | 31 | | | __ _ ___| | __ 32 | | |/ _` / __| |/ / 33 | | | (_| \__ \ < 34 | \_/\__,_|___/_|\_\ 35 | 36 | 37 | _____ _ _ _ 38 | / ___| | | | | | | 39 | \ `--. ___| |__ ___ __| |_ _| | ___ _ __ 40 | `--. \/ __| '_ \ / _ \/ _` | | | | |/ _ \ '__| 41 | /\__/ / (__| | | | __/ (_| | |_| | | __/ | 42 | \____/ \___|_| |_|\___|\__,_|\__,_|_|\___|_| 43 | "; 44 | 45 | _logger.Info(header); 46 | 47 | try 48 | { 49 | _service = new Thread(() => 50 | { 51 | // Start OWIN host 52 | _webServer = WebApp.Start($"http://+:{_port}"); 53 | _logger.Info($"Server running on port {_port}"); 54 | Process.Start($"http://localhost:{_port}/hangfire"); 55 | }); 56 | // _service.SetApartmentState(ApartmentState.STA); // cause to run slower 57 | _service.Priority = ThreadPriority.AboveNormal; 58 | _service.Start(); 59 | 60 | return true; 61 | } 62 | catch (Exception exp) 63 | { 64 | if (exp.InnerException is HttpListenerException) 65 | throw new FieldAccessException( 66 | "Access to listen this port is denied, please run as administrator!", exp.InnerException); 67 | 68 | _logger.Fatal(exp); 69 | Debugger.Break(); 70 | return false; 71 | } 72 | } 73 | 74 | 75 | public bool Stop() 76 | { 77 | try 78 | { 79 | _logger.Info("request to service stopping..."); 80 | _webServer?.Dispose(); 81 | _service?.Join(); 82 | } 83 | catch (Exception e) 84 | { 85 | _logger.Fatal(e); 86 | } 87 | 88 | return true; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/TaskScheduler/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Web.Http; 6 | using AdoManager; 7 | using Hangfire; 8 | using Hangfire.Logging; 9 | using Hangfire.Logging.LogProviders; 10 | using Hangfire.SqlServer; 11 | using Microsoft.Owin; 12 | using Newtonsoft.Json; 13 | using Newtonsoft.Json.Serialization; 14 | using NLog.Owin.Logging; 15 | using Owin; 16 | using TaskScheduler; 17 | using TaskScheduler.Core; 18 | using TaskScheduler.Helper; 19 | using TaskScheduler.Properties; 20 | 21 | [assembly: OwinStartup(typeof(Startup))] 22 | 23 | namespace TaskScheduler 24 | { 25 | public class Startup 26 | { 27 | public void Configuration(IAppBuilder app) 28 | { 29 | // settings will automatically be used by JsonConvert.SerializeObject/DeserializeObject 30 | JsonConvert.DefaultSettings = () => new JsonSerializerSettings 31 | { 32 | Formatting = Formatting.Indented, 33 | ContractResolver = new CamelCasePropertyNamesContractResolver() 34 | }; 35 | 36 | // 37 | // set database connection strings 38 | var connString = Settings.Default.ServerConnectionString; 39 | if (Debugger.IsAttached) 40 | connString = Settings.Default.LocalConnectionString; 41 | 42 | #region Configure NLog middleware 43 | 44 | app.UseNLog(); 45 | LogProvider.SetCurrentLogProvider(new ColouredConsoleLogProvider()); 46 | 47 | #endregion 48 | 49 | #region Configure Web API 50 | 51 | // Configure Web API for self-host. 52 | var config = new HttpConfiguration(); 53 | config.MapHttpAttributeRoutes(); 54 | app.UseWebApi(config); 55 | 56 | #endregion 57 | 58 | #region Configure Hangfire Background Worker 59 | 60 | // Configure AppDomain parameter to simplify the Configure – http://stackoverflow.com/a/3501950/1317575 61 | AppDomain.CurrentDomain.SetData("DataDirectory", 62 | Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data")); 63 | 64 | var script = FileManager.ReadResourceFile(Settings.Default.HangfireDbScript); 65 | var cm = new ConnectionManager(new Connection(connString)); 66 | cm.CreateDatabaseIfNotExist(script); 67 | 68 | GlobalConfiguration.Configuration.UseSqlServerStorage(connString, 69 | new SqlServerStorageOptions { QueuePollInterval = TimeSpan.FromSeconds(30) }) 70 | .UseFilter(new LogEverythingAttribute()); 71 | 72 | app.UseHangfireDashboard("/hangfire"); 73 | app.UseHangfireServer(new BackgroundJobServerOptions 74 | { 75 | ServerCheckInterval = TimeSpan.FromSeconds(30), 76 | HeartbeatInterval = TimeSpan.FromSeconds(5) 77 | /*ServerName = "Hasin_Hangfire"*/ 78 | }, JobStorage.Current); 79 | 80 | // Read and start jobs 81 | JobsManager.CheckupSetting(Settings.Default.SettingFileName, 100); 82 | 83 | #endregion 84 | 85 | 86 | // Alert all notify receivers to know TaskScheduler restarted 87 | if (JobsManager.Setting?.Notifications?.Any() == true) 88 | { 89 | foreach (var notify in JobsManager.Setting.Notifications) 90 | { 91 | notify.Notify("`Application successful running...`", "Task Scheduler {version}"); 92 | } 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/TaskScheduler/TaskScheduler.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {A0A1BFE1-D843-45DB-8884-7C0EA1351670} 8 | Exe 9 | Properties 10 | TaskScheduler 11 | TaskScheduler 12 | v4.6 13 | 512 14 | true 15 | false 16 | 17 | 18 | publish\ 19 | true 20 | Disk 21 | false 22 | Foreground 23 | 7 24 | Days 25 | false 26 | false 27 | true 28 | 0 29 | 1.0.0.%2a 30 | false 31 | true 32 | 33 | 34 | AnyCPU 35 | true 36 | full 37 | false 38 | bin\Output\ 39 | TRACE;DEBUG;#TestWithoutNotify 40 | prompt 41 | 4 42 | 43 | 44 | AnyCPU 45 | pdbonly 46 | true 47 | bin\Output\ 48 | TRACE 49 | prompt 50 | 4 51 | 52 | 53 | 54 | 55 | 56 | 57 | app.manifest 58 | 59 | 60 | 61 | ..\packages\AdoManager.1.1.85\lib\net45\AdoManager.dll 62 | True 63 | 64 | 65 | ..\packages\Dapper.1.50.5\lib\net451\Dapper.dll 66 | 67 | 68 | ..\packages\Hangfire.Core.1.6.21\lib\net45\Hangfire.Core.dll 69 | 70 | 71 | ..\packages\Hangfire.SqlServer.1.6.21\lib\net45\Hangfire.SqlServer.dll 72 | 73 | 74 | ..\packages\Microsoft.Owin.4.0.0\lib\net451\Microsoft.Owin.dll 75 | 76 | 77 | ..\packages\Microsoft.Owin.FileSystems.4.0.0\lib\net451\Microsoft.Owin.FileSystems.dll 78 | 79 | 80 | ..\packages\Microsoft.Owin.Host.HttpListener.4.0.0\lib\net451\Microsoft.Owin.Host.HttpListener.dll 81 | 82 | 83 | ..\packages\Microsoft.Owin.Hosting.4.0.0\lib\net451\Microsoft.Owin.Hosting.dll 84 | 85 | 86 | ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll 87 | 88 | 89 | ..\packages\NLog.4.5.11\lib\net45\NLog.dll 90 | 91 | 92 | ..\packages\NLog.Owin.Logging.1.1.0\lib\net45\NLog.Owin.Logging.dll 93 | 94 | 95 | ..\packages\Owin.1.0\lib\net40\Owin.dll 96 | 97 | 98 | ..\packages\RestSharp.106.5.4\lib\net452\RestSharp.dll 99 | 100 | 101 | ..\packages\RestSharp.Newtonsoft.Json.1.5.0\lib\net452\RestSharp.Serializers.Newtonsoft.Json.dll 102 | 103 | 104 | ..\packages\SlackMessenger.1.0.1\lib\net45\SlackMessenger.dll 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ..\packages\Microsoft.AspNet.WebApi.Client.5.2.6\lib\net45\System.Net.Http.Formatting.dll 114 | 115 | 116 | 117 | 118 | 119 | ..\packages\System.Text.Encodings.Web.4.5.0\lib\netstandard1.0\System.Text.Encodings.Web.dll 120 | 121 | 122 | 123 | ..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll 124 | 125 | 126 | 127 | ..\packages\Microsoft.AspNet.WebApi.Core.5.2.6\lib\net45\System.Web.Http.dll 128 | 129 | 130 | ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.6\lib\net45\System.Web.Http.Owin.dll 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | ..\packages\Telegram.Bot.14.10.0\lib\net45\Telegram.Bot.dll 139 | 140 | 141 | ..\packages\Topshelf.4.1.0\lib\net452\Topshelf.dll 142 | 143 | 144 | ..\packages\Topshelf.NLog.4.1.0\lib\net452\Topshelf.NLog.dll 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | True 178 | True 179 | Settings.settings 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | Always 195 | 196 | 197 | Designer 198 | 199 | 200 | 201 | Designer 202 | 203 | 204 | PreserveNewest 205 | Designer 206 | 207 | 208 | PublicSettingsSingleFileGenerator 209 | Settings.Designer.cs 210 | 211 | 212 | 213 | 214 | False 215 | Microsoft .NET Framework 4.6 %28x86 and x64%29 216 | true 217 | 218 | 219 | False 220 | .NET Framework 3.5 SP1 221 | false 222 | 223 | 224 | 225 | 226 | 227 | 228 | 235 | -------------------------------------------------------------------------------- /src/TaskScheduler/TaskSchedulerSetting.json: -------------------------------------------------------------------------------- 1 | { 2 | "jobs": [ 3 | { 4 | "jobType": "Recurring", 5 | "enable": true, 6 | "name": "max-purchese checker (per hour)", 7 | "description": "Call purchese web api every hour for max purchase", 8 | "actionName": "CallRestApi", 9 | "actionParameters": { 10 | "url": "http://localhost:8002/v1/monitor/highpayment", 11 | "httpMethod": "GET", 12 | "queryParameters": { 13 | "duration": 60, 14 | "maxTotal": 200, 15 | "maxPerUser": 20 16 | } 17 | }, 18 | "notifyCondition": "NotEquals", 19 | "notifyConditionResult": "OK: \"\"", 20 | "triggerOn": "0 * * * *", // every hour 21 | "notifications": [ 22 | { 23 | "notificationServiceName": "testSlack", 24 | "receiver": "alerts;" 25 | } 26 | ] 27 | }, 28 | { 29 | "jobType": "Recurring", 30 | "enable": true, 31 | "name": "download checker", 32 | "description": "Call download web api every 30 minute", 33 | "actionName": "CallRestApi", 34 | "actionParameters": { 35 | "url": "http://localhost:8002/v1/monitor/download", 36 | "httpMethod": "GET", 37 | "queryParameters": { 38 | "duration": 30, 39 | "value": 4.0 40 | } 41 | }, 42 | "notifyCondition": "NotEquals", 43 | "notifyConditionResult": "OK: \"\"", 44 | "triggerOn": "*/30 * * * *" // every 30 minutes 45 | }, 46 | { 47 | "jobType": "Recurring", 48 | "enable": true, 49 | "name": "User Account Creates Checker", 50 | "description": "Check users account create count per minutes", 51 | "actionName": "CallRestApi", 52 | "actionParameters": { 53 | "url": "http://localhost:8002/v1/monitor/account", 54 | "queryParameters": { 55 | "duration": 60, 56 | "min": 0 57 | }, 58 | "httpMethod": "GET" 59 | }, 60 | "notifyCondition": "NotEquals", 61 | "notifyConditionResult": "OK: \"\"", 62 | "triggerOn": "0 */2 * * *" // every 2 hours 63 | }, 64 | { 65 | "jobType": "Recurring", 66 | "enable": true, 67 | "name": "log checker", 68 | "description": "Remove more than 7 days ago files", 69 | "actionName": "StartProgram", 70 | "actionParameters": { 71 | "fileName": "forfiles", 72 | "arguments": "/p \"C:\\inetpub\\logs\\LogFiles\" /s /m *.* /c \"cmd /c Del @path\" /d -7", 73 | "windowsStyle": "Hidden", 74 | "createNoWindow": true, 75 | "useShellExecute": false 76 | }, 77 | "triggerOn": "0 12 * * 5" // At 12:00 PM, Only on Friday 78 | } 79 | ], 80 | "notifications": [ 81 | { 82 | "notificationServiceName": "RahyabSmsService", 83 | "receiver": "09354028149;09123456789;" 84 | }, 85 | { 86 | "notificationServiceName": "taaghcheMailService", 87 | "receiver": "bezzad@gmail.com;" 88 | }, 89 | { 90 | "notificationServiceName": "testSlack", 91 | "receiver": "alerts" 92 | }, 93 | { 94 | "notificationServiceName": "testTelegram", 95 | "receiver": "106752126, @telester" 96 | } 97 | ], 98 | "notificationServices": [ 99 | { 100 | "serviceName": "RahyabSmsService", 101 | "notificationType": "SmsService", 102 | "username": "1000", 103 | "password": "TakTak", 104 | "senderNumber": "50001760", 105 | "isDefaultService": true 106 | }, 107 | { 108 | "serviceName": "taaghcheMailService", 109 | "notificationType": "EmailService", 110 | "smtpPassword": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 111 | "smtpSenderEmail": "postmaster@test.com", 112 | "smtpUrl": "smtp.mailgun.org", 113 | "smtpPort": 587, 114 | "logo": "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", 115 | "logoUrl": null, 116 | "clickUrl": "https://test.com", 117 | "unsubscribeUrl": "http://api.test.com/unsubscribe/?mail={receiver}", 118 | "isDefaultService": true 119 | }, 120 | { 121 | "serviceName": "testTelegram", 122 | "notificationType": "TelegramService", 123 | "username": "@telesterbot", 124 | "apiKey": "684394290:AAFg3Ht1EtNB7iYe9V_VVxKCEVMP-lSjycA", 125 | "senderBot": "@telesterbot", 126 | "isDefaultService": true 127 | }, 128 | { 129 | "serviceName": "testSlack", 130 | "notificationType": "SlackService", 131 | "senderName": "TaskScheduler {#version}", 132 | "webhookUrl": "https://hooks.slack.com/services/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", 133 | "iconUrl": ":robot_face:", // or set URL like: "https://a.slack-edge.com/9c217/img/loading_hash_animation.gif" 134 | "isDefaultService": true 135 | }, 136 | { 137 | "serviceName": "CallRestApi", 138 | "notificationType": "CallRestApiService", 139 | "clientId": "api", 140 | "clientSecret": "XXXXXXXXXXXXXXXXXXXX", 141 | "authServerUrl": "https://auth.test.com/connect/token", 142 | "authGrantType": "client_credentials", 143 | "authScope": "service", 144 | "isDefaultService": true 145 | } 146 | ] 147 | } -------------------------------------------------------------------------------- /src/TaskScheduler/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 59 | 60 | 61 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/TaskScheduler/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------
194 | /// using NotificationServices.EmailService; 195 | /// 196 | /// if (EmailService.ValidateEmailAddressSyntax("_mike.o'neil_loves_underscores@sub-domain.travel")) 197 | /// { 198 | /// Console.WriteLine("Correct syntax"); 199 | /// } 200 | /// else 201 | /// { 202 | /// Console.WriteLine("Wrong syntax"); 203 | /// } 204 | ///
44 | {subject} 45 |
51 |
52 | {message} 53 |