[mod] e-mails plus explicites (titre + motif erreur)
This commit is contained in:
@@ -6,6 +6,8 @@ import * as os from 'os'
|
|||||||
|
|
||||||
export async function sendMail(conf: ConfMail, subject: string, html: string, cc?: string) {
|
export async function sendMail(conf: ConfMail, subject: string, html: string, cc?: string) {
|
||||||
|
|
||||||
|
console.log(`[mail] Subject: ${subject}`)
|
||||||
|
console.log(`[mail] Body: ${html}`)
|
||||||
if (conf.enabled) {
|
if (conf.enabled) {
|
||||||
let transporter = nodemailer.createTransport({
|
let transporter = nodemailer.createTransport({
|
||||||
host: conf.host,
|
host: conf.host,
|
||||||
@@ -38,23 +40,22 @@ export const mail = {
|
|||||||
</p>
|
</p>
|
||||||
`) => {
|
`) => {
|
||||||
return async (cc?: string) => {
|
return async (cc?: string) => {
|
||||||
console.log(`${message}`)
|
await sendMail(conf.mail, `[dw] [${os.hostname}] ${message}`, getHtml(), cc)
|
||||||
await sendMail(conf.mail, `[dwatcher] [${os.hostname}] ${message}`, getHtml(), cc)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onDisconnect: (conf: Conf, target: string, message = `Connection closed for ${target}`, getHtml: (waitingDelay: number) => string = (waitingDelay: number) => `
|
onDisconnect: (conf: Conf, target: string, message = `Connection closed for ${target}`, getErrorMessage: () => string = () => '', getHtml: (waitingDelay: number) => string = (waitingDelay: number) => `
|
||||||
<p>
|
<p>
|
||||||
Connection from [${os.hostname}] to ${target} was lost on ${moment().format('dd-MM-YYYY HH:mm:ss')}.
|
Connection from [${os.hostname}] to ${target} was lost on ${moment().format('dd-MM-YYYY HH:mm:ss')}.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Waiting ${(waitingDelay / 1000).toFixed(0)} seconds before trying to reconnect.
|
Waiting ${(waitingDelay / 1000).toFixed(0)} seconds before trying to reconnect.
|
||||||
</p>
|
</p>
|
||||||
|
${getErrorMessage()}
|
||||||
`) => {
|
`) => {
|
||||||
return async (waitingDelay: number, cc?: string) => {
|
return async (waitingDelay: number, cc?: string) => {
|
||||||
console.log(`${message}`)
|
|
||||||
console.log('Waiting %s seconds...', (waitingDelay / 1000).toFixed(0))
|
console.log('Waiting %s seconds...', (waitingDelay / 1000).toFixed(0))
|
||||||
await sendMail(conf.mail, `[dwatcher] [${os.hostname}] ${message}`, getHtml(waitingDelay), cc)
|
await sendMail(conf.mail, `[dw] [${os.hostname}] ${message}`, getHtml(waitingDelay), cc)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -65,21 +66,7 @@ export const mail = {
|
|||||||
`) => {
|
`) => {
|
||||||
return async (cc?: string) => {
|
return async (cc?: string) => {
|
||||||
console.log(`${message}`)
|
console.log(`${message}`)
|
||||||
await sendMail(conf.mail, `[dwatcher] [${os.hostname}] ${message}`, getHtml(), cc)
|
await sendMail(conf.mail, `[dw] [${os.hostname}] ${message}`, getHtml(), cc)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (conf: Conf, target: string) => {
|
|
||||||
return async (e: Error) => {
|
|
||||||
console.error(e.message || e)
|
|
||||||
// await sendMail(conf.mail, `[dwatcher] Connection error`, `
|
|
||||||
// <p>
|
|
||||||
// Connection from [${os.hostname}] error with ${target} on ${moment().format('dd-MM-YYYY HH:mm:ss')}:
|
|
||||||
// </p>
|
|
||||||
// <p>
|
|
||||||
// ${e.message}
|
|
||||||
// </p>
|
|
||||||
// `)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
export class Watcher {
|
export class Watcher {
|
||||||
private state: 'INIT'|'OK'|'FAILURE'|'RECOVERED'
|
private _state: 'INIT'|'OK'|'FAILURE'|'RECOVERED'
|
||||||
private failureMessage?: string
|
private _error?: any
|
||||||
|
|
||||||
constructor(public readonly name: string) {
|
constructor(public readonly name: string) {
|
||||||
this.state = "INIT"
|
this._state = "INIT"
|
||||||
}
|
}
|
||||||
|
|
||||||
public stateOK() {
|
public stateOK() {
|
||||||
this.state = "OK"
|
this._state = "OK"
|
||||||
}
|
}
|
||||||
|
|
||||||
public stateRecovered() {
|
public stateRecovered() {
|
||||||
this.state = "RECOVERED"
|
this._state = "RECOVERED"
|
||||||
}
|
}
|
||||||
|
|
||||||
public stateFailure(error: string) {
|
public stateFailure(error: any) {
|
||||||
this.state = "FAILURE"
|
this._state = "FAILURE"
|
||||||
this.failureMessage = error
|
this._error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
public get error() {
|
||||||
|
return this._error
|
||||||
|
}
|
||||||
|
|
||||||
|
public get failureMessage(): string|undefined {
|
||||||
|
if (!this._error) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return this._error.message || this._error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ export function watcherLoop(
|
|||||||
onConnectionClosed: () => Promise<void>,
|
onConnectionClosed: () => Promise<void>,
|
||||||
reconnectionDelays: number[],
|
reconnectionDelays: number[],
|
||||||
onStart: () => Promise<void>,
|
onStart: () => Promise<void>,
|
||||||
onDisconnection: (waitingDelay: number) => Promise<void>,
|
onDisconnection: (waitingDelay: number, error: any) => Promise<void>,
|
||||||
onRestart: () => Promise<void>,
|
onRestart: () => Promise<void>,
|
||||||
onRestartSuccess: () => Promise<void>,
|
onRestartSuccess: () => Promise<void>,
|
||||||
onError: (e: Error) => Promise<void>,
|
|
||||||
): Watcher {
|
): Watcher {
|
||||||
|
|
||||||
let watcher: Watcher = new Watcher(name)
|
let watcher: Watcher = new Watcher(name)
|
||||||
@@ -44,12 +43,11 @@ export function watcherLoop(
|
|||||||
await onConnectionClosed()
|
await onConnectionClosed()
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await onError(e)
|
watcher.stateFailure(e)
|
||||||
watcher.stateFailure(e && e.message || e)
|
|
||||||
}
|
}
|
||||||
// Wait before reconnecting
|
// Wait before reconnecting
|
||||||
const waitingDelay = reconnectionDelays[Math.min(reconnectionDelays.length - 1, i)]
|
const waitingDelay = reconnectionDelays[Math.min(reconnectionDelays.length - 1, i)]
|
||||||
await onDisconnection(waitingDelay)
|
await onDisconnection(waitingDelay, watcher.error)
|
||||||
await new Promise(resolve => setTimeout(resolve, waitingDelay))
|
await new Promise(resolve => setTimeout(resolve, waitingDelay))
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,23 +4,33 @@ import Axios from "axios";
|
|||||||
import {mail} from "../../mail";
|
import {mail} from "../../mail";
|
||||||
import {Watcher} from "../../types/state";
|
import {Watcher} from "../../types/state";
|
||||||
|
|
||||||
export function urlWatcher(conf: Conf, checkValidity: (data: any) => Promise<void>) {
|
export function urlWatcher(conf: Conf, checkValidity: (data: any) => Promise<UrlWatcherResult>) {
|
||||||
|
|
||||||
return async (urlConf: ConfURL): Promise<Watcher> => {
|
return async (urlConf: ConfURL,
|
||||||
|
getOkTitle: () => string,
|
||||||
|
getKoTitle: () => string,
|
||||||
|
getRecoveredTitle: () => string): Promise<Watcher> => {
|
||||||
|
|
||||||
let nodeDownRes: () => void
|
let nodeDownRes: () => void
|
||||||
let nodeDownPromise: Promise<void> = new Promise(res => nodeDownRes = res)
|
let nodeDownPromise: Promise<void> = new Promise(res => nodeDownRes = res)
|
||||||
|
|
||||||
|
async function checkResult(data: any) {
|
||||||
|
const validity = await checkValidity(data)
|
||||||
|
if (validity.error) {
|
||||||
|
throw new UrlWatcherError('Validity error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return watcherLoop(
|
return watcherLoop(
|
||||||
urlConf.name,
|
urlConf.name,
|
||||||
async () => {
|
async () => {
|
||||||
let interval: NodeJS.Timer;
|
let interval: NodeJS.Timer;
|
||||||
const res = await Axios.get(urlConf.address)
|
const res = await Axios.get(urlConf.address)
|
||||||
await checkValidity(res.data)
|
await checkResult(res.data)
|
||||||
interval = setInterval(async () => {
|
interval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await Axios.get(urlConf.address)
|
const res = await Axios.get(urlConf.address)
|
||||||
await checkValidity(res.data)
|
await checkResult(res.data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (interval) {
|
if (interval) {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
@@ -36,21 +46,45 @@ export function urlWatcher(conf: Conf, checkValidity: (data: any) => Promise<voi
|
|||||||
|
|
||||||
conf.reconnectionDelays,
|
conf.reconnectionDelays,
|
||||||
|
|
||||||
mail.onEstablished(conf, urlConf.address),
|
mail.onEstablished(conf, urlConf.address, getOkTitle()),
|
||||||
|
|
||||||
// When a disconnection is detected
|
// When a disconnection is detected
|
||||||
mail.onDisconnect(conf, urlConf.address),
|
(waitingDelay: number, error?: any) => {
|
||||||
|
let koTitle: string|undefined
|
||||||
|
let koMessage: (() => string)|undefined
|
||||||
|
if (error && error instanceof UrlWatcherError) {
|
||||||
|
koTitle = getKoTitle()
|
||||||
|
koMessage = () => `<p>${error.errorMessage}</p>`
|
||||||
|
}
|
||||||
|
return mail.onDisconnect(conf, urlConf.address, koTitle, koMessage)(waitingDelay)
|
||||||
|
},
|
||||||
|
|
||||||
async () => {
|
async () => {
|
||||||
console.log('Trying to connect to %s', urlConf.address)
|
console.log('Trying to connect to %s', urlConf.address)
|
||||||
},
|
},
|
||||||
|
|
||||||
mail.onRestartSuccess(conf, urlConf.address),
|
mail.onRestartSuccess(conf, urlConf.address, getRecoveredTitle()),
|
||||||
|
|
||||||
async (e) => {
|
|
||||||
console.error(e.message || e)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UrlWatcherError {
|
||||||
|
|
||||||
|
constructor (public errorMessage: string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UrlWatcherResult {
|
||||||
|
error?: string
|
||||||
|
|
||||||
|
public static ok() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ko(errorMessage: string) {
|
||||||
|
return { error: errorMessage }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Conf, ConfBMA} from "../../types/conf";
|
import {Conf, ConfBMA} from "../../types/conf";
|
||||||
import {urlWatcher} from '../abstract/url-watcher'
|
import {urlWatcher, UrlWatcherResult} from '../abstract/url-watcher'
|
||||||
import {moment} from 'duniter/app/lib/common-libs/moment'
|
import {moment} from 'duniter/app/lib/common-libs/moment'
|
||||||
|
|
||||||
export function bmaWatcher(conf: Conf) {
|
export function bmaWatcher(conf: Conf) {
|
||||||
@@ -11,12 +11,16 @@ export function bmaWatcher(conf: Conf) {
|
|||||||
return urlWatcher(conf, async (data) => {
|
return urlWatcher(conf, async (data) => {
|
||||||
const block = data as { medianTime: number }
|
const block = data as { medianTime: number }
|
||||||
if (bmaServer.maxLate && moment().unix() - block.medianTime > bmaServer.maxLate) {
|
if (bmaServer.maxLate && moment().unix() - block.medianTime > bmaServer.maxLate) {
|
||||||
throw 'Server is late'
|
return UrlWatcherResult.ko('Server is late')
|
||||||
}
|
}
|
||||||
|
return UrlWatcherResult.ok()
|
||||||
})({
|
})({
|
||||||
name: `BMA ${bmaServer.address}`,
|
name: `BMA ${bmaServer.address}`,
|
||||||
address: bmaServer.address + URL_PATH,
|
address: bmaServer.address + URL_PATH,
|
||||||
frequency: bmaServer.frequency
|
frequency: bmaServer.frequency
|
||||||
})
|
},
|
||||||
|
() => `[OK] BMA of ${bmaServer.address}`,
|
||||||
|
() => `[FAILURE] BMA of ${bmaServer.address}`,
|
||||||
|
() => `[RECOVERED] BMA of ${bmaServer.address}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import {Conf, ConfHead} from "../../types/conf";
|
import {Conf, ConfHead} from "../../types/conf";
|
||||||
import {urlWatcher} from '../abstract/url-watcher'
|
import {urlWatcher, UrlWatcherResult} from '../abstract/url-watcher'
|
||||||
import {WS2PHead} from "duniter/app/modules/ws2p/lib/WS2PCluster";
|
import {WS2PHead} from "duniter/app/modules/ws2p/lib/WS2PCluster";
|
||||||
|
|
||||||
function handleLateness(confHead: ConfHead, mainHeads: HeadMetric[], observedHead?: TrameWS2P) {
|
function handleLateness(confHead: ConfHead, mainHeads: HeadMetric[], observedHead?: TrameWS2P): UrlWatcherResult {
|
||||||
if (!mainHeads.length) {
|
if (!mainHeads.length) {
|
||||||
throw 'Observed pubkey ${confHead.observedPubkey}: no consensus found'
|
return UrlWatcherResult.ko('Observed pubkey ${confHead.observedPubkey}: no consensus found')
|
||||||
}
|
}
|
||||||
if (!observedHead) {
|
if (!observedHead) {
|
||||||
throw `Observed pubkey ${confHead.observedPubkey} not found in heads`
|
return UrlWatcherResult.ko(`Observed pubkey ${confHead.observedPubkey} not found in heads`)
|
||||||
}
|
}
|
||||||
const matchingHeads = mainHeads.filter(h => h.blockstamp === observedHead.blockstamp)
|
const matchingHeads = mainHeads.filter(h => h.blockstamp === observedHead.blockstamp)
|
||||||
if (!matchingHeads.length) {
|
if (!matchingHeads.length) {
|
||||||
// Check how much late is the node
|
// Check how much late is the node
|
||||||
const farAwayHeads = mainHeads.filter(h => h.blockNumber - observedHead.blockNumber >= confHead.maxLateBlocks)
|
const farAwayHeads = mainHeads.filter(h => h.blockNumber - observedHead.blockNumber >= confHead.maxLateBlocks)
|
||||||
if (farAwayHeads.length) {
|
if (farAwayHeads.length) {
|
||||||
throw `Observed pubkey ${confHead.observedPubkey} is too late for ${farAwayHeads.length} consensus by at least ${confHead.maxLateBlocks}`
|
return UrlWatcherResult.ko(`Observed pubkey ${confHead.observedPubkey} is too late for ${farAwayHeads.length} consensus by at least ${confHead.maxLateBlocks}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return UrlWatcherResult.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function headWatcher(conf: Conf) {
|
export function headWatcher(conf: Conf) {
|
||||||
|
|
||||||
const URL_PATH = '/network/ws2p/heads'
|
const URL_PATH = '/network/ws2p/heads'
|
||||||
|
const KEY_LENGTH = 10
|
||||||
|
|
||||||
return async (confHead: ConfHead) => {
|
return async (confHead: ConfHead) => {
|
||||||
|
|
||||||
@@ -29,12 +31,15 @@ export function headWatcher(conf: Conf) {
|
|||||||
const heads = data as { heads: WS2PHead[] }
|
const heads = data as { heads: WS2PHead[] }
|
||||||
const mainHeads = getMain(heads)
|
const mainHeads = getMain(heads)
|
||||||
const observedHead = getObserved(heads, confHead.observedPubkey)
|
const observedHead = getObserved(heads, confHead.observedPubkey)
|
||||||
handleLateness(confHead, mainHeads, observedHead)
|
return handleLateness(confHead, mainHeads, observedHead)
|
||||||
})({
|
})({
|
||||||
name: `head watcher ${confHead.address} => ${confHead.observedPubkey.substr(0, 10)}`,
|
name: `head watcher ${confHead.address} => ${confHead.observedPubkey.substr(0, KEY_LENGTH)}`,
|
||||||
address: confHead.address + URL_PATH,
|
address: confHead.address + URL_PATH,
|
||||||
frequency: confHead.frequency
|
frequency: confHead.frequency
|
||||||
})
|
},
|
||||||
|
() => `[OK] HEADS of ${confHead.observedPubkey.substr(0, KEY_LENGTH)}`,
|
||||||
|
() => `[FAILURE] HEADS of ${confHead.observedPubkey.substr(0, KEY_LENGTH)}`,
|
||||||
|
() => `[RECOVERED] HEADS of ${confHead.observedPubkey.substr(0, KEY_LENGTH)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Conf, ConfDprobeHeartbeat} from "../../types/conf";
|
import {Conf, ConfDprobeHeartbeat} from "../../types/conf";
|
||||||
import {urlWatcher} from '../abstract/url-watcher'
|
import {urlWatcher, UrlWatcherResult} from '../abstract/url-watcher'
|
||||||
import {moment} from 'duniter/app/lib/common-libs/moment'
|
import {moment} from 'duniter/app/lib/common-libs/moment'
|
||||||
|
|
||||||
export function dprobeHeartbeat(conf: Conf) {
|
export function dprobeHeartbeat(conf: Conf) {
|
||||||
@@ -10,8 +10,12 @@ export function dprobeHeartbeat(conf: Conf) {
|
|||||||
const last = moment(data, 'YYYY-MM-DD HH:mm:ss\n')
|
const last = moment(data, 'YYYY-MM-DD HH:mm:ss\n')
|
||||||
const past = moment().diff(last)
|
const past = moment().diff(last)
|
||||||
if (past > dconf.lastBeat) {
|
if (past > dconf.lastBeat) {
|
||||||
throw 'Delay is over'
|
return UrlWatcherResult.ko('Delay is over')
|
||||||
}
|
}
|
||||||
})(dconf)
|
return UrlWatcherResult.ok()
|
||||||
|
})(dconf,
|
||||||
|
() => `[OK] hearbeat is up-to-date`,
|
||||||
|
() => `[FAILURE] hearbeat`,
|
||||||
|
() => `[RECOVERED] hearbeat`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function webDiffWatcher(conf: Conf) {
|
|||||||
() => mail.onEstablished(conf, target, 'webdiff successfully started')(webDiffConf.cc),
|
() => mail.onEstablished(conf, target, 'webdiff successfully started')(webDiffConf.cc),
|
||||||
|
|
||||||
// When a disconnection is detected
|
// When a disconnection is detected
|
||||||
(waitingDelay: number) => mail.onDisconnect(conf, target, 'Diff detected', (waitingDelay: number) => `
|
(waitingDelay: number) => mail.onDisconnect(conf, target, 'Diff detected', undefined, (waitingDelay: number) => `
|
||||||
${htmlDiff}
|
${htmlDiff}
|
||||||
<p>
|
<p>
|
||||||
Waiting ${(waitingDelay / 1000).toFixed(0)} seconds before trying to reconnect.
|
Waiting ${(waitingDelay / 1000).toFixed(0)} seconds before trying to reconnect.
|
||||||
@@ -78,10 +78,6 @@ export function webDiffWatcher(conf: Conf) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
() => mail.onRestartSuccess(conf, target)(webDiffConf.cc),
|
() => mail.onRestartSuccess(conf, target)(webDiffConf.cc),
|
||||||
|
|
||||||
async (e) => {
|
|
||||||
console.error(e.message)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import {Conf, ConfWWMeta} from "../../types/conf";
|
import {Conf, ConfWWMeta} from "../../types/conf";
|
||||||
import {urlWatcher} from '../abstract/url-watcher'
|
import {urlWatcher, UrlWatcherResult} from '../abstract/url-watcher'
|
||||||
|
|
||||||
function handleLateness(confHead: ConfWWMeta, data: WWMetaJson) {
|
function handleLateness(confHead: ConfWWMeta, data: WWMetaJson) {
|
||||||
const diff = Math.round(Date.now()/1000 - data.now)
|
const diff = Math.round(Date.now()/1000 - data.now)
|
||||||
if (diff >= confHead.maxLate) {
|
if (diff >= confHead.maxLate) {
|
||||||
throw `WWMeta.json is late by ${diff}s (>= ${confHead.maxLate})`
|
return UrlWatcherResult.ko(`WWMeta.json is late by ${diff}s (>= ${confHead.maxLate})`)
|
||||||
}
|
}
|
||||||
|
return UrlWatcherResult.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jsonWatcher(conf: Conf) {
|
export function jsonWatcher(conf: Conf) {
|
||||||
@@ -15,12 +16,15 @@ export function jsonWatcher(conf: Conf) {
|
|||||||
return async (confWWMeta: ConfWWMeta) => {
|
return async (confWWMeta: ConfWWMeta) => {
|
||||||
|
|
||||||
return urlWatcher(conf, async (data) => {
|
return urlWatcher(conf, async (data) => {
|
||||||
handleLateness(confWWMeta, data)
|
return handleLateness(confWWMeta, data)
|
||||||
})({
|
})({
|
||||||
name: `WWMeta.json watcher ${confWWMeta.address}`,
|
name: `WWMeta.json watcher ${confWWMeta.address}`,
|
||||||
address: confWWMeta.address + URL_PATH,
|
address: confWWMeta.address + URL_PATH,
|
||||||
frequency: confWWMeta.frequency
|
frequency: confWWMeta.frequency
|
||||||
})
|
},
|
||||||
|
() => `[OK] WWMeta.json is up-to-date`,
|
||||||
|
() => `[FAILURE] WWMeta.json`,
|
||||||
|
() => `[RECOVERED] WWMeta.json`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ export function ws2pWatcher(conf: Conf) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
mail.onRestartSuccess(conf, target),
|
mail.onRestartSuccess(conf, target),
|
||||||
|
|
||||||
mail.onError(conf, target)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user