mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 06:58:52 +08:00
fix: batch notification audits and bump CLI to 0.1.3
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@whyour/qinglong-cli",
|
"name": "@whyour/qinglong-cli",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@whyour/qinglong-cli",
|
"name": "@whyour/qinglong-cli",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ql": "dist/npm/ql.js"
|
"ql": "dist/npm/ql.js"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@whyour/qinglong-cli",
|
"name": "@whyour/qinglong-cli",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"description": "Authenticated remote management CLI for QingLong 2.x",
|
"description": "Authenticated remote management CLI for QingLong 2.x",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+17
-5
@@ -1313,30 +1313,40 @@ def add_notify_function():
|
|||||||
|
|
||||||
|
|
||||||
def report_notification(channel, title, content, success):
|
def report_notification(channel, title, content, success):
|
||||||
|
report_notifications([(channel, success)], title, content)
|
||||||
|
|
||||||
|
|
||||||
|
def report_notifications(results, title, content):
|
||||||
ql_dir = os.getenv("QL_DIR")
|
ql_dir = os.getenv("QL_DIR")
|
||||||
if not ql_dir:
|
if not ql_dir or not results:
|
||||||
return
|
return
|
||||||
payload = {"title": str(title or "")[:512], "content": str(content or "")[:8192],
|
payload = [{"title": str(title or "")[:512], "content": str(content or "")[:8192],
|
||||||
"success": success, "notifyType": channel,
|
"success": success, "notifyType": channel,
|
||||||
"scriptName": os.path.basename(__import__("sys").argv[0]), "taskId": os.getenv("ID", "")}
|
"scriptName": os.path.basename(__import__("sys").argv[0]), "taskId": os.getenv("ID", "")}
|
||||||
|
for channel, success in results]
|
||||||
code = """const fs=require('node:fs'),path=require('node:path');
|
code = """const fs=require('node:fs'),path=require('node:path');
|
||||||
const report=require(path.join(process.env.QL_DIR,'shell/preload/notificationAudit.js'));
|
const report=require(path.join(process.env.QL_DIR,'shell/preload/notificationAudit.js'));
|
||||||
report(JSON.parse(fs.readFileSync(0,'utf8'))).catch(()=>{});"""
|
report(JSON.parse(fs.readFileSync(0,'utf8'))).catch(()=>{});"""
|
||||||
try:
|
try:
|
||||||
|
# One Node startup/TLS connection per send, with room for cold startup
|
||||||
|
# before the reporter's bounded RPC deadline on busy Alpine hosts.
|
||||||
subprocess.run(["node", "-e", code], input=json.dumps(payload), text=True,
|
subprocess.run(["node", "-e", code], input=json.dumps(payload), text=True,
|
||||||
capture_output=True, timeout=3)
|
capture_output=True, timeout=10)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Audit availability never changes notification delivery.
|
pass # Audit availability never changes notification delivery.
|
||||||
|
|
||||||
|
|
||||||
def audited_notification(mode, title, content):
|
def audited_notification(mode, title, content, results=None):
|
||||||
result = None
|
result = None
|
||||||
try:
|
try:
|
||||||
result = mode(title, content)
|
result = mode(title, content)
|
||||||
except Exception:
|
except Exception:
|
||||||
result = False
|
result = False
|
||||||
print(f"{mode.__name__} 推送失败")
|
print(f"{mode.__name__} 推送失败")
|
||||||
|
if results is None:
|
||||||
report_notification(mode.__name__, title, content, result is True)
|
report_notification(mode.__name__, title, content, result is True)
|
||||||
|
else:
|
||||||
|
results.append((mode.__name__, result is True))
|
||||||
|
|
||||||
|
|
||||||
def send(title: str, content: str, ignore_default_config: bool = False, **kwargs):
|
def send(title: str, content: str, ignore_default_config: bool = False, **kwargs):
|
||||||
@@ -1362,12 +1372,14 @@ def send(title: str, content: str, ignore_default_config: bool = False, **kwargs
|
|||||||
content += "\n\n" + one() if hitokoto not in [False, "false"] else ""
|
content += "\n\n" + one() if hitokoto not in [False, "false"] else ""
|
||||||
|
|
||||||
notify_function = add_notify_function()
|
notify_function = add_notify_function()
|
||||||
|
results = []
|
||||||
ts = [
|
ts = [
|
||||||
threading.Thread(target=audited_notification, args=(mode, title, content), name=mode.__name__)
|
threading.Thread(target=audited_notification, args=(mode, title, content, results), name=mode.__name__)
|
||||||
for mode in notify_function
|
for mode in notify_function
|
||||||
]
|
]
|
||||||
[t.start() for t in ts]
|
[t.start() for t in ts]
|
||||||
[t.join() for t in ts]
|
[t.join() for t in ts]
|
||||||
|
report_notifications(results, title, content)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -27,14 +27,22 @@ module.exports = async function reportNotificationAudit(payload) {
|
|||||||
credentials,
|
credentials,
|
||||||
{ 'grpc.enable_http_proxy': 0 },
|
{ 'grpc.enable_http_proxy': 0 },
|
||||||
);
|
);
|
||||||
await new Promise((resolve) => {
|
// Python sends all channel results together so they share startup and TLS.
|
||||||
|
const records = Array.isArray(payload) ? payload : [payload];
|
||||||
|
const deadline = new Date(Date.now() + 5000);
|
||||||
|
await Promise.all(
|
||||||
|
records.map(
|
||||||
|
(record) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
client.RecordNotifyLog(
|
client.RecordNotifyLog(
|
||||||
payload,
|
record,
|
||||||
new grpc.Metadata(),
|
new grpc.Metadata(),
|
||||||
{ deadline: new Date(Date.now() + 1500) },
|
{ deadline },
|
||||||
() => resolve(),
|
() => resolve(),
|
||||||
);
|
);
|
||||||
});
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Missing certificates, old panels and offline services are best effort.
|
// Missing certificates, old panels and offline services are best effort.
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ test('JS and Python script reports reach the same authenticated RPC with bounded
|
|||||||
server.addService(Api.service, {
|
server.addService(Api.service, {
|
||||||
RecordNotifyLog: (call, callback) => {
|
RecordNotifyLog: (call, callback) => {
|
||||||
received.push(call.request);
|
received.push(call.request);
|
||||||
callback(null, { code: 200 });
|
if (call.request.title === 'Concurrent cold startup')
|
||||||
|
setTimeout(() => callback(null, { code: 200 }), 2000);
|
||||||
|
else callback(null, { code: 200 });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const credentials = grpc.ServerCredentials.createSsl(
|
const credentials = grpc.ServerCredentials.createSsl(
|
||||||
@@ -91,6 +93,46 @@ n.send('Python failure','body')
|
|||||||
);
|
);
|
||||||
assert.ok(received.every((x) => x.taskId === '9'));
|
assert.ok(received.every((x) => x.taskId === '9'));
|
||||||
assert.ok(!JSON.stringify(received).includes('private-password'));
|
assert.ok(!JSON.stringify(received).includes('private-password'));
|
||||||
|
// A busy host can spend longer than the former 3s timeout starting Node.
|
||||||
|
// All channels must still arrive, using one child and one TLS connection.
|
||||||
|
const coldStart = path.join(data, 'cold-start.cjs');
|
||||||
|
fs.writeFileSync(
|
||||||
|
coldStart,
|
||||||
|
'Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,3500);',
|
||||||
|
);
|
||||||
|
const concurrent = `import importlib.util,sys,types,os,threading,json,subprocess
|
||||||
|
sys.modules['requests']=types.SimpleNamespace()
|
||||||
|
spec=importlib.util.spec_from_file_location('notify',os.path.join(os.environ['QL_DIR'],'sample/notify.py'))
|
||||||
|
n=importlib.util.module_from_spec(spec);spec.loader.exec_module(n)
|
||||||
|
barrier=threading.Barrier(3)
|
||||||
|
def accepted(title,content):
|
||||||
|
barrier.wait(timeout=5);return True
|
||||||
|
def rejected(title,content):
|
||||||
|
barrier.wait(timeout=5);raise RuntimeError('private-password')
|
||||||
|
def no_result(title,content):
|
||||||
|
barrier.wait(timeout=5)
|
||||||
|
n.add_notify_function=lambda:[accepted,rejected,no_result]
|
||||||
|
original=subprocess.run
|
||||||
|
children=[]
|
||||||
|
def tracked(*args,**kwargs):
|
||||||
|
children.append(json.loads(kwargs['input']))
|
||||||
|
return original(*args,**kwargs)
|
||||||
|
n.subprocess.run=tracked
|
||||||
|
n.send('Concurrent cold startup','body')
|
||||||
|
assert len(children)==1,children
|
||||||
|
assert len(children[0])==3,children
|
||||||
|
`;
|
||||||
|
await run('python3', ['-c', concurrent], {
|
||||||
|
env: { ...env, NODE_OPTIONS: `--require=${coldStart}` },
|
||||||
|
timeout: 14000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
received
|
||||||
|
.filter((x) => x.title === 'Concurrent cold startup')
|
||||||
|
.map((x) => [x.notifyType, x.success])
|
||||||
|
.sort(),
|
||||||
|
[['accepted', true], ['no_result', false], ['rejected', false]],
|
||||||
|
);
|
||||||
// Missing certificates and an older/unavailable backend must not kill scripts.
|
// Missing certificates and an older/unavailable backend must not kill scripts.
|
||||||
await run(
|
await run(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
@@ -110,5 +152,5 @@ n.send('Python failure','body')
|
|||||||
],
|
],
|
||||||
{ env, timeout: 8000 },
|
{ env, timeout: 8000 },
|
||||||
);
|
);
|
||||||
assert.ok(Date.now() - started < 5000);
|
assert.ok(Date.now() - started < 7000);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user