@@ -3,12 +3,13 @@ import { Injectable, Logger } from '@nestjs/common'
33import { SchedulerRegistry } from '@nestjs/schedule'
44import { CronHooksService } from './cron-hooks.service'
55import { pick } from 'radash'
6- import { CronTaskDTO } from './_dto/config-task.dto'
6+ import { ConfigTaskDTO , CronTaskDTO } from './_dto/config-task.dto'
77import { CronJob } from 'cron'
88import path from 'node:path'
9- import { existsSync , readFileSync , statSync } from 'node:fs'
9+ import { closeSync , existsSync , openSync , readFileSync , readSync , readdirSync , statSync , writeFileSync } from 'node:fs'
1010import { ConfigService } from '@nestjs/config'
1111import { toSafeHandlerName } from '~/_common/functions/handler-logger'
12+ import { parse , stringify } from 'yaml'
1213
1314@Injectable ( )
1415export class CronService {
@@ -118,6 +119,29 @@ export class CronService {
118119 }
119120 }
120121
122+ public async setEnabled ( name : string , enabled : boolean ) : Promise < CronTaskDTO & { _job : Partial < CronJob > } | null > {
123+ const updated = this . updateTaskInConfig ( name , ( task ) => {
124+ task . enabled = enabled
125+ } )
126+
127+ if ( ! updated ) {
128+ return null
129+ }
130+
131+ await this . cronHooksService . syncCronJobs ( )
132+ return this . read ( name )
133+ }
134+
135+ public async runImmediately ( name : string ) : Promise < boolean > {
136+ const task = await this . read ( name )
137+ if ( ! task ) {
138+ return false
139+ }
140+
141+ await this . cronHooksService . runTaskNow ( name )
142+ return true
143+ }
144+
121145 public async readLogs ( name : string , tail = 500 ) : Promise < {
122146 name : string
123147 exists : boolean
@@ -140,10 +164,46 @@ export class CronService {
140164 }
141165
142166 const stats = statSync ( logFile )
143- const fullContent = readFileSync ( logFile , 'utf-8' )
144- const boundedTail = Math . min ( Math . max ( tail || 200 , 1 ) , 2_000 )
167+ const boundedTail = Math . max ( tail || 200 , 1 )
145168 const maxLineChars = 4_000
146- const maxContentChars = 200_000
169+ const maxContentChars = this . configService . get < number > ( 'cron.logRotateMaxSizeBytes' ) || 10 * 1024 * 1024
170+ const maxReadableBytes = Math . max ( maxContentChars , 1 )
171+ const chunkSize = 64 * 1024
172+
173+ // Read the file backwards in chunks to avoid loading everything in memory.
174+ const fileDescriptor = openSync ( logFile , 'r' )
175+ let filePosition = stats . size
176+ let bytesReadTotal = 0
177+ let lineBreakCount = 0
178+ const chunks : Buffer [ ] = [ ]
179+
180+ try {
181+ while ( filePosition > 0 && bytesReadTotal < maxReadableBytes && lineBreakCount <= boundedTail ) {
182+ const remainingBudget = maxReadableBytes - bytesReadTotal
183+ const readSize = Math . min ( chunkSize , filePosition , remainingBudget )
184+ filePosition -= readSize
185+
186+ const chunkBuffer = Buffer . allocUnsafe ( readSize )
187+ const readBytes = readSync ( fileDescriptor , chunkBuffer , 0 , readSize , filePosition )
188+ if ( readBytes <= 0 ) {
189+ break
190+ }
191+
192+ const chunk = readBytes === readSize ? chunkBuffer : chunkBuffer . subarray ( 0 , readBytes )
193+ chunks . unshift ( chunk )
194+ bytesReadTotal += readBytes
195+
196+ for ( let index = 0 ; index < chunk . length ; index ++ ) {
197+ if ( chunk [ index ] === 0x0a ) {
198+ lineBreakCount ++
199+ }
200+ }
201+ }
202+ } finally {
203+ closeSync ( fileDescriptor )
204+ }
205+
206+ const fullContent = Buffer . concat ( chunks ) . toString ( 'utf-8' )
147207
148208 const tailedLines = fullContent
149209 . split ( '\n' )
@@ -168,4 +228,35 @@ export class CronService {
168228 content,
169229 }
170230 }
231+
232+ private updateTaskInConfig ( name : string , updater : ( task : CronTaskDTO ) => void ) : boolean {
233+ const configDir = path . join ( process . cwd ( ) , 'configs' , 'cron' )
234+ if ( ! existsSync ( configDir ) ) {
235+ return false
236+ }
237+
238+ const files = readdirSync ( configDir ) . filter ( ( file ) => file . endsWith ( '.yml' ) || file . endsWith ( '.yaml' ) )
239+
240+ for ( const file of files ) {
241+ const filePath = path . join ( configDir , file )
242+ const raw = readFileSync ( filePath , 'utf-8' )
243+ const parsed = parse ( raw ) as ConfigTaskDTO
244+ const tasks = parsed ?. tasks
245+
246+ if ( ! tasks || ! Array . isArray ( tasks ) ) {
247+ continue
248+ }
249+
250+ const targetTask = tasks . find ( ( task ) => task . name === name )
251+ if ( ! targetTask ) {
252+ continue
253+ }
254+
255+ updater ( targetTask )
256+ writeFileSync ( filePath , stringify ( parsed ) )
257+ return true
258+ }
259+
260+ return false
261+ }
171262}
0 commit comments